diff --git a/CHANGELOG.md b/CHANGELOG.md index 13b62ac770..e89df5877d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). [Commits](https://github.com/scalableminds/webknossos/compare/19.03.0...HEAD) ### Added -- +- The dataset settings within the tracing view allow to select between different loading strategies now ("best quality first" and "progressive quality"). Additionally, the rendering can use different magnifications as a fallback (instead of only one magnification). [#3801](https://github.com/scalableminds/webknossos/pull/3801) ### Changed - diff --git a/MIGRATIONS.md b/MIGRATIONS.md index a339aeebc4..f9f75011b1 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -5,7 +5,7 @@ This project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`. User-facing changes are documented in the [changelog](CHANGELOG.md). ## Unreleased -- +- To ensure that the existing behavior for loading data is preserved ("best quality first" as opposed to the new "progressive quality" default) execute: `update webknossos.user_datasetconfigurations set configuration = configuration || jsonb '{"loadingStrategy":"BEST_QUALITY_FIRST"}'`. See [#3801](https://github.com/scalableminds/webknossos/pull/3801) for additional context. ### Postgres Evolutions: - diff --git a/docs/tracing_ui.md b/docs/tracing_ui.md index 3f4b490e55..e201cebc35 100644 --- a/docs/tracing_ui.md +++ b/docs/tracing_ui.md @@ -244,7 +244,7 @@ Not all settings are available in every tracing mode. - `Move Value (nm/s)`: A high value will speed up movement through the dataset, e.g. when holding down the spacebar. Vice-versa, a low value will slow down the movement allowing for more precision. This setting is especially useful in `Flight mode`. -- `d/f-Switching`: ¯\\_(ツ)_/¯ +- `d/f-Switching`: If d/f switching is disabled, moving through the dataset with `f` will always go *f*orward by *increasing* the coordinate orthogonal to the current slice. Correspondingly, `d` will backwards by decreasing that coordinate. However, if d/f is enabled, the meaning of "forward" and "backward" will change depending on how you create nodes. For example, when a node is placed at z == 100 and afterwards another node is created at z == 90, z will be *decreased* when going forward. #### Viewport Options / Flight Options - `Zoom`: The zoom factor for viewing the dataset. A low value moves the camera really close to the data, showing many details. A high value, will you show more of the dataset but with fewer details and is great for getting an overview or moving around quickly. @@ -287,6 +287,8 @@ For multi-layer datasets, each layer can be adjusted separately. - `Highlight Hovered Cells`: Toggles whether segmented cells will be highlighted in all viewports when hovering over them with the mouse cursor. Useful for identifying the highlighted cell in across all viewports. #### Quality -- `4 Bit`: Toggles data download from the server using only 4 Bit instead of 8 Bit for each pixel. Use this to reduce the amount of necessary internet bandwidth for webKnossos. Useful for showcasing data on the go over cellular networks, e.g 3G. - `Quality`: Adjusts the quality level used for data download from the server. "High" will load the original, unmodified data. "Medium" and "Low" will load a downsampled version of the data layer to reduce network traffic. Use this to reduce the amount of necessary internet bandwidth for webKnossos. - +- `Loading Strategy`: You can choose between two different loading strategies. When using "best quality first" it will take a bit longer until you see data, because the highest quality is loaded. Alternatively, "Progressive quality" can be chosen which will improve the quality progressively while loading. As a result, initial data will be visible faster, but it will take more time until the best quality is shown. +- `4 Bit`: Toggles data download from the server using only 4 Bit instead of 8 Bit for each pixel. Use this to reduce the amount of necessary internet bandwidth for webKnossos. Useful for showcasing data on the go over cellular networks, e.g 3G. +- `Interpolation`: When interpolation is enabled, bilinear filtering is applied while rendering pixels between two voxels. As a result, data may look "smoother" (or blurry when being zoomed in very far). Without interpolation, data may look more "crisp" (or pixelated when being zomed in very far). +- `Render Missing Data Black`: If a dataset doesn't contain data at a specific position, webKnossos can either render "black" at that position or it can try to render data from another magnification. diff --git a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js index a1bcf684f7..91fc1a5e0a 100644 --- a/frontend/javascripts/admin/tasktype/recommended_configuration_view.js +++ b/frontend/javascripts/admin/tasktype/recommended_configuration_view.js @@ -36,6 +36,7 @@ const recommendedConfigByCategory = { highlightHoveredCellId: false, zoom: 0.8, renderMissingDataBlack: false, + loadingStrategy: "BEST_QUALITY_FIRST", }, flight: { clippingDistanceArbitrary: 60, @@ -66,6 +67,7 @@ export const settingComments = { quality: "0 (high), 1 (medium), 2 (low)", clippingDistanceArbitrary: "flight/oblique mode", moveValue3d: "flight/oblique mode", + loadingStrategy: "BEST_QUALITY_FIRST or PROGRESSIVE_QUALITY", }; const errorIcon = ( diff --git a/frontend/javascripts/libs/user_settings.schema.json b/frontend/javascripts/libs/user_settings.schema.json index fcaaaeff92..6403757328 100644 --- a/frontend/javascripts/libs/user_settings.schema.json +++ b/frontend/javascripts/libs/user_settings.schema.json @@ -28,6 +28,7 @@ "fourBit": { "type": "boolean" }, "interpolation": { "type": "boolean" }, "quality": { "type": "number", "enum": [0, 1, 2] }, + "loadingStrategy": { "enum": ["BEST_QUALITY_FIRST", "PROGRESSIVE_QUALITY"] }, "segmentationOpacity": { "type": "number", "minimum": 0, "maximum": 100 }, "highlightHoveredCellId": { "type": "boolean" }, "zoom": { "type": "number", "minimum": 0.001 }, diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index 83d2898435..59e5d361f3 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -28,6 +28,11 @@ export const settings = { crosshairSize: "Crosshair Size", brushSize: "Brush Size", userBoundingBox: "Bounding Box", + loadingStrategy: "Loading Strategy", + loadingStrategyDescription: `You can choose between loading the best quality first + (will take longer until you see data) or alternatively, + improving the quality progressively (data will be loaded faster, + but it will take more time until the best quality is shown).`, }; export default { diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index 37874ce234..3d96cbc7b5 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -653,6 +653,13 @@ class DataApi { return segmentationLayer.name; } + /** + * Invalidates all downloaded buckets so that they are reloaded on the next movement. + */ + reloadAllBuckets(): void { + _.forEach(this.model.dataLayers, dataLayer => dataLayer.cube.collectAllBuckets()); + } + /** * Sets a mapping for a given layer. * diff --git a/frontend/javascripts/oxalis/constants.js b/frontend/javascripts/oxalis/constants.js index 265c8ba7a3..1177b62ccf 100644 --- a/frontend/javascripts/oxalis/constants.js +++ b/frontend/javascripts/oxalis/constants.js @@ -122,12 +122,9 @@ const VIEWPORT_WIDTH = 376; export const ensureSmallerEdge = false; // Using the following dimensions for the address space, -// the look up buffer (256**2) is used at a rate of ~ 97% -// ((32 × 32 × 50 + 16 × 16 × 50) / 256^2 = 0.976563) -export const addressSpaceDimensions = { - normal: [32, 32, 50], - fallback: [16, 16, 50], -}; +// the look up buffer (256**2) is used at a rate of ~ 99% +// ((36 × 36 × 50) / 256^2 = 0.98877) +export const addressSpaceDimensions = [36, 36, 50]; export const Unicode = { ThinSpace: "\u202f", @@ -145,8 +142,6 @@ const Constants = { MODES_ARBITRARY: ["flight", "oblique"], MODES_SKELETON: ["orthogonal", "flight", "oblique"], - DEFAULT_SEG_ALPHA: 20, - BUCKET_WIDTH: 32, BUCKET_SIZE: 32 ** 3, VIEWPORT_WIDTH, @@ -158,7 +153,8 @@ const Constants = { MINIMUM_REQUIRED_BUCKET_CAPACITY: 3 * 512, LOOK_UP_TEXTURE_WIDTH: 256, - TDView_MOVE_SPEED: 150, + MAX_ZOOM_STEP_DIFF_PREFETCH: 1, // prefetch only fallback buckets for currentZoomStep + 1 + MIN_MOVE_VALUE: 30, MAX_MOVE_VALUE: 14000, MAX_MOVE_VALUE_SLIDER: 1500, @@ -179,8 +175,6 @@ const Constants = { MIN_PARTICLE_SIZE: 1, MAX_PARTICLE_SIZE: 20, - ZOOM_DIFF: 0.1, - DEFAULT_SPHERICAL_CAP_RADIUS: 140, // !Currently disabled! diff --git a/frontend/javascripts/oxalis/controller/scene_controller.js b/frontend/javascripts/oxalis/controller/scene_controller.js index 1c4670b730..b422c82b51 100644 --- a/frontend/javascripts/oxalis/controller/scene_controller.js +++ b/frontend/javascripts/oxalis/controller/scene_controller.js @@ -337,27 +337,30 @@ class SceneController { // all buckets necessary for rendering are addressed. The anchorPoint is // defined with bucket indices for the coordinate system of the current zoomStep. let anchorPoint; - // The fallbackAnchorPoint is similar to the anchorPoint, but refers to the - // coordinate system of the next zoomStep which is used for fallback rendering. - let fallbackAnchorPoint; const zoomStep = getRequestLogZoomStep(Store.getState()); for (const dataLayer of Model.getAllLayers()) { - [anchorPoint, fallbackAnchorPoint] = dataLayer.layerRenderingManager.updateDataTextures( - globalPosition, - zoomStep, - ); + anchorPoint = dataLayer.layerRenderingManager.updateDataTextures(globalPosition, zoomStep); } if (optArbitraryPlane) { - optArbitraryPlane.updateAnchorPoints(anchorPoint, fallbackAnchorPoint); + optArbitraryPlane.updateAnchorPoints(anchorPoint); optArbitraryPlane.setPosition(globalPosVec); } else { for (const currentPlane of _.values(this.planes)) { - currentPlane.updateAnchorPoints(anchorPoint, fallbackAnchorPoint); + currentPlane.updateAnchorPoints(anchorPoint); currentPlane.setPosition(globalPosVec); const [scaleX, scaleY] = getPlaneScalingFactor(state, flycam, currentPlane.planeID); - currentPlane.setScale(scaleX, scaleY); + const isVisible = scaleX > 0 && scaleY > 0; + if (isVisible) { + this.displayPlane[currentPlane.planeID] = true; + currentPlane.setScale(scaleX, scaleY); + } else { + this.displayPlane[currentPlane.planeID] = false; + // Set the scale to non-zero values, since threejs will otherwise + // complain about non-invertible matrices. + currentPlane.setScale(1, 1); + } } } } diff --git a/frontend/javascripts/oxalis/geometries/arbitrary_plane.js b/frontend/javascripts/oxalis/geometries/arbitrary_plane.js index 4af456c97e..ed2927085e 100644 --- a/frontend/javascripts/oxalis/geometries/arbitrary_plane.js +++ b/frontend/javascripts/oxalis/geometries/arbitrary_plane.js @@ -52,13 +52,10 @@ class ArbitraryPlane { this.materialFactory.stopListening(); } - updateAnchorPoints(anchorPoint: ?Vector4, fallbackAnchorPoint: ?Vector4): void { + updateAnchorPoints(anchorPoint: ?Vector4): void { if (anchorPoint) { this.meshes.mainPlane.material.setAnchorPoint(anchorPoint); } - if (fallbackAnchorPoint) { - this.meshes.mainPlane.material.setFallbackAnchorPoint(fallbackAnchorPoint); - } } setPosition = ({ x, y, z }: THREE.Vector3) => { diff --git a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js index 5d5b4d37e4..61817ed1b4 100644 --- a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js +++ b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory.js @@ -113,10 +113,6 @@ class PlaneMaterialFactory { type: "v4", value: new THREE.Vector3(0, 0, 0), }, - fallbackAnchorPoint: { - type: "v4", - value: new THREE.Vector3(0, 0, 0), - }, zoomStep: { type: "f", value: 1, @@ -187,11 +183,7 @@ class PlaneMaterialFactory { }, addressSpaceDimensions: { type: "v3", - value: new THREE.Vector3(...addressSpaceDimensions.normal), - }, - addressSpaceDimensionsFallback: { - type: "v3", - value: new THREE.Vector3(...addressSpaceDimensions.fallback), + value: new THREE.Vector3(...addressSpaceDimensions), }, }; @@ -290,10 +282,6 @@ class PlaneMaterialFactory { this.uniforms.anchorPoint.value.set(x, y, z); }; - this.material.setFallbackAnchorPoint = ([x, y, z]) => { - this.uniforms.fallbackAnchorPoint.value.set(x, y, z); - }; - this.material.setSegmentationAlpha = alpha => { this.uniforms.alpha.value = alpha / 100; }; diff --git a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory_helpers.js b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory_helpers.js index 07f606d35d..68210c78b3 100644 --- a/frontend/javascripts/oxalis/geometries/materials/plane_material_factory_helpers.js +++ b/frontend/javascripts/oxalis/geometries/materials/plane_material_factory_helpers.js @@ -3,6 +3,13 @@ import * as THREE from "three"; import UpdatableTexture from "libs/UpdatableTexture"; +export const channelCountToFormat = { + "1": THREE.LuminanceFormat, + "2": THREE.LuminanceAlphaFormat, + "3": THREE.RGBFormat, + "4": THREE.RGBAFormat, +}; + // This function has to be in its own file as non-resolvable cycles are created otherwise export function createUpdatableTexture( width: number, @@ -10,16 +17,8 @@ export function createUpdatableTexture( type: THREE.FloatType | THREE.UnsignedByteType | THREE.Uint32BufferAttribute, renderer: THREE.WebGLRenderer, ): UpdatableTexture { - let format; - if (channelCount === 1) { - format = THREE.LuminanceFormat; - } else if (channelCount === 2) { - format = THREE.LuminanceAlphaFormat; - } else if (channelCount === 3) { - format = THREE.RGBFormat; - } else if (channelCount === 4) { - format = THREE.RGBAFormat; - } else { + const format = channelCountToFormat[channelCount]; + if (!format) { throw new Error(`Unhandled byte count: ${channelCount}`); } diff --git a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js index ff997bfba3..70eda56dc7 100644 --- a/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js +++ b/frontend/javascripts/oxalis/model/accessors/flycam_accessor.js @@ -3,7 +3,7 @@ import * as THREE from "three"; import _ from "lodash"; import memoizeOne from "memoize-one"; -import type { Flycam, OxalisState } from "oxalis/store"; +import type { Flycam, LoadingStrategy, OxalisState } from "oxalis/store"; import { M4x4, type Matrix4x4 } from "libs/mjs"; import { ZOOM_STEP_INTERVAL } from "oxalis/model/reducers/flycam_reducer"; import { clamp, map3 } from "libs/utils"; @@ -18,13 +18,14 @@ import constants, { type Vector3, type ViewMode, } from "oxalis/constants"; -import determineBucketsForOrthogonal from "oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker"; import determineBucketsForFlight from "oxalis/model/bucket_data_handling/bucket_picker_strategies/flight_bucket_picker"; import determineBucketsForOblique from "oxalis/model/bucket_data_handling/bucket_picker_strategies/oblique_bucket_picker"; +import determineBucketsForOrthogonal from "oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker"; import * as scaleInfo from "oxalis/model/scaleinfo"; function calculateTotalBucketCountForZoomLevel( viewMode: ViewMode, + loadingStrategy: LoadingStrategy, datasetScale: Vector3, resolutions: Array, logZoomStep: number, @@ -39,7 +40,6 @@ function calculateTotalBucketCountForZoomLevel( // Define dummy values const position = [0, 0, 0]; const anchorPoint = [0, 0, 0, 0]; - const fallbackAnchorPoint = [0, 0, 0, 0]; const subBucketLocality = [1, 1, 1]; const sphericalCapRadius = constants.DEFAULT_SPHERICAL_CAP_RADIUS; @@ -76,9 +76,9 @@ function calculateTotalBucketCountForZoomLevel( determineBucketsForOrthogonal( resolutions, enqueueFunction, + loadingStrategy, logZoomStep, anchorPoint, - fallbackAnchorPoint, areas, subBucketLocality, abortLimit, @@ -101,6 +101,7 @@ function calculateTotalBucketCountForZoomLevel( // This function is only exported for testing purposes export function _getMaximumZoomForAllResolutions( viewMode: ViewMode, + loadingStrategy: LoadingStrategy, datasetScale: Vector3, resolutions: Array, viewportRects: OrthoViewRects, @@ -129,6 +130,7 @@ export function _getMaximumZoomForAllResolutions( const nextZoomValue = maxZoomValue * ZOOM_STEP_INTERVAL; const nextCapacity = calculateTotalBucketCountForZoomLevel( viewMode, + loadingStrategy, datasetScale, resolutions, currentResolutionIndex, @@ -196,6 +198,7 @@ export function getRequestLogZoomStep(state: OxalisState): number { const { viewMode } = state.temporaryConfiguration; const maximumZoomSteps = getMaximumZoomForAllResolutions( viewMode, + state.datasetConfiguration.loadingStrategy, state.dataset.dataSource.scale, getResolutions(state.dataset), getViewportRects(state), @@ -222,6 +225,7 @@ export function getMaxZoomValue(state: OxalisState): number { const maximumZoomSteps = getMaximumZoomForAllResolutions( viewMode, + state.datasetConfiguration.loadingStrategy, state.dataset.dataSource.scale, getResolutions(state.dataset), getViewportRects(state), diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js index 6f01a9b824..54471cd975 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket.js @@ -7,9 +7,13 @@ import BackboneEvents from "backbone-events-standalone"; import * as THREE from "three"; import _ from "lodash"; -import { bucketPositionToGlobalAddress } from "oxalis/model/helpers/position_converter"; +import { + bucketPositionToGlobalAddress, + zoomedAddressToAnotherZoomStep, +} from "oxalis/model/helpers/position_converter"; import { getRequestLogZoomStep } from "oxalis/model/accessors/flycam_accessor"; import { getResolutions } from "oxalis/model/accessors/dataset_accessor"; +import DataCube from "oxalis/model/bucket_data_handling/data_cube"; import Store from "oxalis/store"; import TemporalBucketManager from "oxalis/model/bucket_data_handling/temporal_bucket_manager"; import Toast from "libs/toast"; @@ -32,14 +36,46 @@ const warnAboutDownsamplingRGB = _.once(() => ); export const bucketDebuggingFlags = { - // DEBUG flag for visualizing buckets which are passed to the GPU + // For visualizing buckets which are passed to the GPU visualizeBucketsOnGPU: false, - // DEBUG flag for visualizing buckets which are prefetched + // For visualizing buckets which are prefetched visualizePrefetchedBuckets: false, + // For enforcing fallback rendering. enforcedZoomDiff == 2, means + // that buckets of currentZoomStep + 2 are rendered. + enforcedZoomDiff: undefined, }; // Exposing this variable allows debugging on deployed systems window.bucketDebuggingFlags = bucketDebuggingFlags; +export class NullBucket { + type: "null" = "null"; + isOutOfBoundingBox: boolean; + + constructor(isOutOfBoundingBox: boolean) { + this.isOutOfBoundingBox = isOutOfBoundingBox; + } + + hasData(): boolean { + return false; + } + + needsRequest(): boolean { + return false; + } + + getData(): Uint8Array { + throw new Error("NullBucket has no data."); + } +} + +export const NULL_BUCKET = new NullBucket(false); +export const NULL_BUCKET_OUT_OF_BB = new NullBucket(true); + +// The type is used within the DataBucket class which is why +// we have to define it here. +// eslint-disable-next-line no-use-before-define +export type Bucket = DataBucket | NullBucket; + export class DataBucket { type: "data" = "data"; BIT_DEPTH: number; @@ -60,27 +96,28 @@ export class DataBucket { on: Function; off: Function; once: Function; + cube: DataCube; // For downsampled buckets, "dependentBucketListenerSet" stores the // buckets to which a listener is already attached // Remove once https://github.com/babel/babel-eslint/pull/584 is merged - // eslint-disable-next-line no-use-before-define dependentBucketListenerSet: WeakSet = new WeakSet(); // We cannot use dependentBucketListenerSet.length for that, since WeakSets don't hold that information dependentCounter: number = 0; // For downsampled buckets, "isDirtyDueToDependent" stores the buckets // due to which the current bucket is dirty and need new downsampling - // Remove once https://github.com/babel/babel-eslint/pull/584 is merged - // eslint-disable-next-line no-use-before-define isDirtyDueToDependent: WeakSet = new WeakSet(); isDownSampled: boolean; + _fallbackBucket: ?Bucket; constructor( BIT_DEPTH: number, zoomedAddress: Vector4, temporalBucketManager: TemporalBucketManager, + cube: DataCube, ) { _.extend(this, BackboneEvents); + this.cube = cube; this.BIT_DEPTH = BIT_DEPTH; this.BUCKET_LENGTH = (1 << (BUCKET_SIZE_P * 3)) * (this.BIT_DEPTH >> 3); this.BYTE_OFFSET = this.BIT_DEPTH >> 3; @@ -105,6 +142,15 @@ export class DataBucket { return collect; } + destroy(): void { + // Since we rely on the GC to collect buckets, we + // can easily have references to buckets which prohibit GC. + // As a countermeasure, we set the data attribute to null + // so that at least the big memory hog is tamed (unfortunately, + // this doesn't help against references which point directly to this.data) + this.data = null; + } + needsRequest(): boolean { return this.state === BucketStateEnum.UNREQUESTED; } @@ -136,7 +182,7 @@ export class DataBucket { getData(): Uint8Array { const data = this.data; if (data == null) { - throw new Error("Bucket.getData() called, but data does not exist."); + throw new Error("Bucket.getData() called, but data does not exist (anymore)."); } return data; @@ -214,6 +260,36 @@ export class DataBucket { throw new Error(`Unexpected state: ${this.state}`); } + getFallbackBucket(): Bucket { + if (this._fallbackBucket != null) { + return this._fallbackBucket; + } + const zoomStep = this.zoomedAddress[3]; + const fallbackZoomStep = zoomStep + 1; + const resolutions = getResolutions(Store.getState().dataset); + + if (fallbackZoomStep >= resolutions.length) { + this._fallbackBucket = NULL_BUCKET; + return NULL_BUCKET; + } + + const fallbackBucketAddress = zoomedAddressToAnotherZoomStep( + this.zoomedAddress, + resolutions, + fallbackZoomStep, + ); + const fallbackBucket = this.cube.getOrCreateBucket(fallbackBucketAddress); + + this._fallbackBucket = fallbackBucket; + if (fallbackBucket.type !== "null") { + fallbackBucket.once("bucketCollected", () => { + this._fallbackBucket = null; + }); + } + + return fallbackBucket; + } + downsampleFromLowerBucket( bucket: DataBucket, resolutionsFactors: Vector3, @@ -381,29 +457,3 @@ export class DataBucket { } } } - -export class NullBucket { - type: "null" = "null"; - isOutOfBoundingBox: boolean; - - constructor(isOutOfBoundingBox: boolean) { - this.isOutOfBoundingBox = isOutOfBoundingBox; - } - - hasData(): boolean { - return false; - } - - needsRequest(): boolean { - return false; - } - - getData(): Uint8Array { - throw new Error("NullBucket has no data."); - } -} - -export const NULL_BUCKET = new NullBucket(false); -export const NULL_BUCKET_OUT_OF_BB = new NullBucket(true); - -export type Bucket = DataBucket | NullBucket; diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker.js b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker.js index 82f2cf40d6..8aebaa684c 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/bucket_picker_strategies/orthogonal_bucket_picker.js @@ -1,6 +1,7 @@ // @flow import { type Area } from "oxalis/model/accessors/flycam_accessor"; import type { EnqueueFunction } from "oxalis/model/bucket_data_handling/layer_rendering_manager"; +import type { LoadingStrategy } from "oxalis/store"; import { type OrthoViewMap, OrthoViewValuesWithoutTDView, @@ -8,6 +9,10 @@ import { type Vector4, addressSpaceDimensions, } from "oxalis/constants"; +import { + getMaxZoomStepDiff, + getPriorityWeightForZoomStepDiff, +} from "oxalis/model/bucket_data_handling/loading_strategy_logic"; import { zoomedAddressToAnotherZoomStep } from "oxalis/model/helpers/position_converter"; import Dimensions from "oxalis/model/dimensions"; import ThreeDMap from "libs/ThreeDMap"; @@ -26,48 +31,47 @@ export const getAnchorPositionToCenterDistance = (bucketPerDim: number) => export default function determineBucketsForOrthogonal( resolutions: Array, enqueueFunction: EnqueueFunction, + loadingStrategy: LoadingStrategy, logZoomStep: number, anchorPoint: Vector4, - fallbackAnchorPoint: Vector4, areas: OrthoViewMap, subBucketLocality: Vector3, abortLimit?: number, ) { - addNecessaryBucketsToPriorityQueueOrthogonal( - resolutions, - enqueueFunction, - logZoomStep, - anchorPoint, - false, - areas, - subBucketLocality, - abortLimit, - ); - - if (logZoomStep + 1 < resolutions.length) { + let zoomStepDiff = 0; + + while ( + logZoomStep + zoomStepDiff < resolutions.length && + zoomStepDiff <= getMaxZoomStepDiff(loadingStrategy) + ) { addNecessaryBucketsToPriorityQueueOrthogonal( resolutions, enqueueFunction, - logZoomStep + 1, - fallbackAnchorPoint, - true, + loadingStrategy, + logZoomStep, + zoomStepDiff, + anchorPoint, areas, subBucketLocality, abortLimit, ); + zoomStepDiff++; } } function addNecessaryBucketsToPriorityQueueOrthogonal( resolutions: Array, enqueueFunction: EnqueueFunction, - logZoomStep: number, - zoomedAnchorPoint: Vector4, - isFallback: boolean, + loadingStrategy: LoadingStrategy, + nonFallbackLogZoomStep: number, + zoomStepDiff: number, + nonFallbackAnchorPoint: Vector4, areas: OrthoViewMap, subBucketLocality: Vector3, abortLimit: ?number, ): void { + const logZoomStep = nonFallbackLogZoomStep + zoomStepDiff; + const isFallback = zoomStepDiff > 0; const uniqueBucketMap = new ThreeDMap(); let currentCount = 0; @@ -93,14 +97,11 @@ function addNecessaryBucketsToPriorityQueueOrthogonal( logZoomStep, ); - const bucketsPerDim = isFallback - ? addressSpaceDimensions.fallback - : addressSpaceDimensions.normal; + const renderedBucketsPerDimension = addressSpaceDimensions[w]; - const renderedBucketsPerDimension = bucketsPerDim[w]; - - const topLeftBucket = zoomedAnchorPoint.slice(); + let topLeftBucket = ((nonFallbackAnchorPoint.slice(): any): Vector4); topLeftBucket[w] += getAnchorPositionToCenterDistance(renderedBucketsPerDimension); + topLeftBucket = zoomedAddressToAnotherZoomStep(topLeftBucket, resolutions, logZoomStep); const centerBucketUV = [ scaledTopLeftVector[u] + (scaledBottomRightVector[u] - scaledTopLeftVector[u]) / 2, @@ -112,8 +113,10 @@ function addNecessaryBucketsToPriorityQueueOrthogonal( // Similar to `extraBucketPerEdge`, the PQ takes care of cases in which the additional slice // can't be loaded. const wSliceOffsets = isFallback ? [0] : [0, subBucketLocality[w]]; - // fallback buckets should have lower priority - const additionalPriorityWeight = isFallback ? 1000 : 0; + const additionalPriorityWeight = getPriorityWeightForZoomStepDiff( + loadingStrategy, + zoomStepDiff, + ); // Build up priority queue // eslint-disable-next-line no-loop-func @@ -147,6 +150,7 @@ function addNecessaryBucketsToPriorityQueueOrthogonal( if (uniqueBucketMap.get(bucketVector3) == null) { uniqueBucketMap.set(bucketVector3, bucketAddress); currentCount++; + if (abortLimit != null && currentCount > abortLimit) { return; } diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js index 4bf2c225a0..3694ac48df 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/data_cube.js @@ -247,7 +247,7 @@ class DataCube { } createBucket(address: Vector4): Bucket { - const bucket = new DataBucket(this.BIT_DEPTH, address, this.temporalBucketManager); + const bucket = new DataBucket(this.BIT_DEPTH, address, this.temporalBucketManager, this); bucket.on({ bucketLoaded: () => this.trigger("bucketLoaded", address), }); @@ -311,6 +311,7 @@ class DataCube { const bucketIndex = this.getBucketIndex(address); const cube = this.cubes[address[3]]; if (bucketIndex != null && cube != null) { + bucket.destroy(); cube.data.delete(bucketIndex); } } diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js b/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js index 74cc9af91a..d56fe505cc 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/layer_rendering_manager.js @@ -82,13 +82,7 @@ export default class LayerRenderingManager { cube: DataCube; pullQueue: PullQueue; dataTextureCount: number; - anchorPointCache: { - anchorPoint: Vector4, - fallbackAnchorPoint: Vector4, - } = { - anchorPoint: [0, 0, 0, 0], - fallbackAnchorPoint: [0, 0, 0, 0], - }; + cachedAnchorPoint: Vector4 = [0, 0, 0, 0]; name: string; isSegmentation: boolean; @@ -138,43 +132,30 @@ export default class LayerRenderingManager { } // Returns the new anchorPoints if they are new - updateDataTextures(position: Vector3, logZoomStep: number): [?Vector4, ?Vector4] { - const { dataset } = Store.getState(); - const isAnchorPointNew = this.maybeUpdateAnchorPoint( - position, - logZoomStep, - dataset.dataSource.scale, - false, - ); + updateDataTextures(position: Vector3, logZoomStep: number): ?Vector4 { + const state = Store.getState(); + const { dataset, datasetConfiguration } = state; + const isAnchorPointNew = this.maybeUpdateAnchorPoint(position, logZoomStep); const fallbackZoomStep = logZoomStep + 1; const isFallbackAvailable = fallbackZoomStep <= this.cube.MAX_ZOOM_STEP; - const isFallbackAnchorPointNew = isFallbackAvailable - ? this.maybeUpdateAnchorPoint(position, fallbackZoomStep, dataset.dataSource.scale, true) - : false; if (logZoomStep > this.cube.MAX_ZOOM_STEP) { // Don't render anything if the zoomStep is too high - this.textureBucketManager.setActiveBuckets( - [], - this.anchorPointCache.anchorPoint, - this.anchorPointCache.fallbackAnchorPoint, - ); - return [this.anchorPointCache.anchorPoint, this.anchorPointCache.fallbackAnchorPoint]; + this.textureBucketManager.setActiveBuckets([], this.cachedAnchorPoint); + return this.cachedAnchorPoint; } const subBucketLocality = getSubBucketLocality(position, getResolutions(dataset)[logZoomStep]); - const areas = getAreasFromState(Store.getState()); + const areas = getAreasFromState(state); - const matrix = getZoomedMatrix(Store.getState().flycam); + const matrix = getZoomedMatrix(state.flycam); - const { viewMode } = Store.getState().temporaryConfiguration; + const { viewMode } = state.temporaryConfiguration; const isArbitrary = constants.MODES_ARBITRARY.includes(viewMode); - const { sphericalCapRadius } = Store.getState().userConfiguration; - const isInvisible = - this.isSegmentation && Store.getState().datasetConfiguration.segmentationOpacity === 0; + const { sphericalCapRadius } = state.userConfiguration; + const isInvisible = this.isSegmentation && datasetConfiguration.segmentationOpacity === 0; if ( isAnchorPointNew || - isFallbackAnchorPointNew || !_.isEqual(areas, this.lastAreas) || !_.isEqual(subBucketLocality, this.lastSubBucketLocality) || (isArbitrary && !_.isEqual(this.lastZoomedMatrix, matrix)) || @@ -202,7 +183,7 @@ export default class LayerRenderingManager { }; if (!isInvisible) { - const resolutions = getResolutions(Store.getState().dataset); + const resolutions = getResolutions(dataset); if (viewMode === constants.MODE_ARBITRARY_PLANE) { determineBucketsForOblique( resolutions, @@ -228,9 +209,9 @@ export default class LayerRenderingManager { determineBucketsForOrthogonal( resolutions, enqueueFunction, + datasetConfiguration.loadingStrategy, logZoomStep, - this.anchorPointCache.anchorPoint, - this.anchorPointCache.fallbackAnchorPoint, + this.cachedAnchorPoint, areas, subBucketLocality, ); @@ -248,11 +229,7 @@ export default class LayerRenderingManager { // This tells the bucket collection, that the buckets are necessary for rendering buckets.forEach(b => b.markAsNeeded()); - this.textureBucketManager.setActiveBuckets( - buckets, - this.anchorPointCache.anchorPoint, - this.anchorPointCache.fallbackAnchorPoint, - ); + this.textureBucketManager.setActiveBuckets(buckets, this.cachedAnchorPoint); // In general, pull buckets which are not available but should be sent to the GPU const missingBuckets = bucketsWithPriorities @@ -264,22 +241,14 @@ export default class LayerRenderingManager { this.pullQueue.pull(); } - return [this.anchorPointCache.anchorPoint, this.anchorPointCache.fallbackAnchorPoint]; + return this.cachedAnchorPoint; } - maybeUpdateAnchorPoint( - position: Vector3, - logZoomStep: number, - datasetScale: Vector3, - isFallback: boolean, - ): boolean { + maybeUpdateAnchorPoint(position: Vector3, logZoomStep: number): boolean { const resolutions = getResolutions(Store.getState().dataset); const resolution = resolutions[logZoomStep]; - const bucketsPerDim = isFallback - ? addressSpaceDimensions.fallback - : addressSpaceDimensions.normal; - const maximumRenderedBucketsHalfInVoxel = bucketsPerDim.map( + const maximumRenderedBucketsHalfInVoxel = addressSpaceDimensions.map( bucketPerDim => getAnchorPositionToCenterDistance(bucketPerDim) * constants.BUCKET_WIDTH, ); @@ -292,11 +261,10 @@ export default class LayerRenderingManager { const anchorPoint = this.cube.positionToZoomedAddress(anchorPointInVoxel, logZoomStep); - const cacheKey = isFallback ? "fallbackAnchorPoint" : "anchorPoint"; - if (_.isEqual(anchorPoint, this.anchorPointCache[cacheKey])) { + if (_.isEqual(anchorPoint, this.cachedAnchorPoint)) { return false; } - this.anchorPointCache[cacheKey] = anchorPoint; + this.cachedAnchorPoint = anchorPoint; return true; } } diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/loading_strategy_logic.js b/frontend/javascripts/oxalis/model/bucket_data_handling/loading_strategy_logic.js new file mode 100644 index 0000000000..a3be737809 --- /dev/null +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/loading_strategy_logic.js @@ -0,0 +1,37 @@ +// @flow + +import type { LoadingStrategy } from "oxalis/store"; + +const MAX_ZOOM_STEP_DIFF_QUALITY_FIRST = 1; +const MAX_ZOOM_STEP_DIFF_PROGRESSIVE_QUALITY = 3; + +const zoomStepMultiplier = 1000; + +export function getMaxZoomStepDiff(strategy: LoadingStrategy): number { + if (strategy === "BEST_QUALITY_FIRST") { + return MAX_ZOOM_STEP_DIFF_QUALITY_FIRST; + } else { + return MAX_ZOOM_STEP_DIFF_PROGRESSIVE_QUALITY; + } +} + +export function getPriorityWeightForZoomStepDiff( + strategy: LoadingStrategy, + zoomStepDiff: number, +): number { + // Low numbers equal high priority + if (strategy === "BEST_QUALITY_FIRST") { + return zoomStepDiff * zoomStepMultiplier; + } else { + return (MAX_ZOOM_STEP_DIFF_PROGRESSIVE_QUALITY - zoomStepDiff) * zoomStepMultiplier; + } +} + +export function getPriorityWeightForPrefetch(): number { + const maxMaxZoomStepDiff = Math.max( + MAX_ZOOM_STEP_DIFF_PROGRESSIVE_QUALITY, + MAX_ZOOM_STEP_DIFF_QUALITY_FIRST, + ); + // Always schedule prefetch requests after the bucket picker priorities + return (maxMaxZoomStepDiff + 1) * zoomStepMultiplier; +} diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/prefetch_strategy_plane.js b/frontend/javascripts/oxalis/model/bucket_data_handling/prefetch_strategy_plane.js index d1d1a8a2bc..0362dfbf95 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/prefetch_strategy_plane.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/prefetch_strategy_plane.js @@ -3,18 +3,19 @@ import _ from "lodash"; import type { Area } from "oxalis/model/accessors/flycam_accessor"; -import { +import type { PullQueueItem } from "oxalis/model/bucket_data_handling/pullqueue"; +import { zoomedAddressToAnotherZoomStep } from "oxalis/model/helpers/position_converter"; +import type DataCube from "oxalis/model/bucket_data_handling/data_cube"; +import Dimensions from "oxalis/model/dimensions"; +import constants, { type OrthoView, type OrthoViewMap, OrthoViewValuesWithoutTDView, type Vector3, } from "oxalis/constants"; -import type { PullQueueItem } from "oxalis/model/bucket_data_handling/pullqueue"; -import { zoomedAddressToAnotherZoomStep } from "oxalis/model/helpers/position_converter"; -import type DataCube from "oxalis/model/bucket_data_handling/data_cube"; -import Dimensions from "oxalis/model/dimensions"; +import { getPriorityWeightForPrefetch } from "oxalis/model/bucket_data_handling/loading_strategy_logic"; -const MAX_ZOOM_STEP_DIFF = 1; +const { MAX_ZOOM_STEP_DIFF_PREFETCH } = constants; export class AbstractPrefetchStrategy { velocityRangeStart: number = 0; @@ -130,7 +131,7 @@ export class PrefetchStrategy extends AbstractPrefetchStrategy { ): Array { const pullQueue = []; - if (zoomStepDiff > MAX_ZOOM_STEP_DIFF) { + if (zoomStepDiff > MAX_ZOOM_STEP_DIFF_PREFETCH) { return pullQueue; } @@ -161,12 +162,14 @@ export class PrefetchStrategy extends AbstractPrefetchStrategy { const height = scaledWidthHeightVector[v]; const bucketPositions = this.getBucketPositions(centerBucket3, width, height); + const prefetchWeight = getPriorityWeightForPrefetch(); for (const bucket of bucketPositions) { const priority = Math.abs(bucket[0] - centerBucket3[0]) + Math.abs(bucket[1] - centerBucket3[1]) + Math.abs(bucket[2] - centerBucket3[2]) + + prefetchWeight + fallbackPriorityWeight; pullQueue.push({ bucket: [bucket[0], bucket[1], bucket[2], zoomStep], priority }); if (plane === activePlane) { diff --git a/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js b/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js index 10f87e10b0..d5d7b280eb 100644 --- a/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js +++ b/frontend/javascripts/oxalis/model/bucket_data_handling/texture_bucket_manager.js @@ -4,8 +4,10 @@ import _ from "lodash"; import { DataBucket, bucketDebuggingFlags } from "oxalis/model/bucket_data_handling/bucket"; import { createUpdatableTexture } from "oxalis/geometries/materials/plane_material_factory_helpers"; +import { getMaxZoomStepDiff } from "oxalis/model/bucket_data_handling/loading_strategy_logic"; import { getRenderer } from "oxalis/controller/renderer"; import { waitForCondition } from "libs/utils"; +import Store from "oxalis/store"; import UpdatableTexture from "libs/UpdatableTexture"; import constants, { type Vector4, addressSpaceDimensions } from "oxalis/constants"; import window from "libs/window"; @@ -28,7 +30,7 @@ const lookUpBufferWidth = constants.LOOK_UP_TEXTURE_WIDTH; // If f >= 0, f denotes the index in the data texture where the bucket is stored. // If f == -1, the bucket is not yet committed // If f == -2, the bucket is not supposed to be rendered. Out of bounds. -export const floatsPerLookUpEntry = 1; +export const channelCountForLookupBuffer = 2; export default class TextureBucketManager { dataTextures: Array; @@ -44,7 +46,6 @@ export default class TextureBucketManager { isRefreshBufferOutOfDate: boolean = false; currentAnchorPoint: Vector4 = [0, 0, 0, 0]; - fallbackAnchorPoint: Vector4 = [0, 0, 0, 0]; writerQueue: Array<{ bucket: DataBucket, _index: number }> = []; textureWidth: number; dataTextureCount: number; @@ -59,7 +60,7 @@ export default class TextureBucketManager { this.maximumCapacity = (this.packingDegree * dataTextureCount * textureWidth ** 2) / constants.BUCKET_SIZE; // the look up buffer is addressSpaceDimensions**3 so that arbitrary look ups can be made - const lookUpBufferSize = Math.pow(lookUpBufferWidth, 2) * floatsPerLookUpEntry; + const lookUpBufferSize = Math.pow(lookUpBufferWidth, 2) * channelCountForLookupBuffer; this.textureWidth = textureWidth; this.dataTextureCount = dataTextureCount; @@ -79,7 +80,7 @@ export default class TextureBucketManager { } clear() { - this.setActiveBuckets([], [0, 0, 0, 0], [0, 0, 0, 0]); + this.setActiveBuckets([], [0, 0, 0, 0]); } freeBucket(bucket: DataBucket): void { @@ -98,14 +99,9 @@ export default class TextureBucketManager { // Takes an array of buckets (relative to an anchorPoint) and ensures that these // are written to the dataTexture. The lookUpTexture will be updated to reflect the // new buckets. - setActiveBuckets( - buckets: Array, - anchorPoint: Vector4, - fallbackAnchorPoint: Vector4, - ): void { + setActiveBuckets(buckets: Array, anchorPoint: Vector4): void { this.currentAnchorPoint = anchorPoint; window.currentAnchorPoint = anchorPoint; - this.fallbackAnchorPoint = fallbackAnchorPoint; // Find out which buckets are not needed anymore const freeBucketSet = new Set(this.activeBucketToIndexMap.keys()); for (const bucket of buckets) { @@ -212,7 +208,7 @@ export default class TextureBucketManager { const lookUpTexture = createUpdatableTexture( lookUpBufferWidth, - 1, + channelCountForLookupBuffer, THREE.FloatType, getRenderer(), ); @@ -264,14 +260,64 @@ export default class TextureBucketManager { } _refreshLookUpBuffer() { - // Completely re-write the lookup buffer. This could be smarter, but it's - // probably not worth it. + /* This method completely completely re-writes the lookup buffer. This could be smarter, but it's + * probably not worth it. + * It works as follows: + * - write -2 into the entire buffer as a fallback + * - iterate over all buckets which should be available to the GPU + * - only consider the buckets in the native zoomStep (=> zoomStep === 0) + * - if the current bucket was committed, write the address for that bucket into the look up buffer + * - otherwise, check whether the bucket's fallback bucket is committed so that this can be written into + * the look up buffer (repeat for the next fallback if the bucket wasn't committed). + */ + this.lookUpBuffer.fill(-2); - for (const [bucket, address] of this.activeBucketToIndexMap.entries()) { + const maxZoomStepDiff = getMaxZoomStepDiff( + Store.getState().datasetConfiguration.loadingStrategy, + ); + + const currentZoomStep = this.currentAnchorPoint[3]; + for (const [bucket, reservedAddress] of this.activeBucketToIndexMap.entries()) { + if (bucket.zoomedAddress[3] > currentZoomStep) { + // only write high-res buckets (if a bucket is missing, the fallback bucket will then be written + // into the look up buffer) + continue; + } const lookUpIdx = this._getBucketIndex(bucket); - this.lookUpBuffer[floatsPerLookUpEntry * lookUpIdx] = this.committedBucketSet.has(bucket) - ? address - : -1; + const posInBuffer = channelCountForLookupBuffer * lookUpIdx; + + let address = -1; + let bucketZoomStep = bucket.zoomedAddress[3]; + if (!bucketDebuggingFlags.enforcedZoomDiff && this.committedBucketSet.has(bucket)) { + address = reservedAddress; + } else { + let fallbackBucket = bucket.getFallbackBucket(); + let abortFallbackLoop = false; + const maxAllowedZoomStep = + currentZoomStep + (bucketDebuggingFlags.enforcedZoomDiff || maxZoomStepDiff); + + while (!abortFallbackLoop) { + if (fallbackBucket.type !== "null") { + if ( + fallbackBucket.zoomedAddress[3] <= maxAllowedZoomStep && + this.committedBucketSet.has(fallbackBucket) + ) { + address = this.activeBucketToIndexMap.get(fallbackBucket); + address = address != null ? address : -1; + bucketZoomStep = fallbackBucket.zoomedAddress[3]; + abortFallbackLoop = true; + } else { + // Try next fallback bucket + fallbackBucket = fallbackBucket.getFallbackBucket(); + } + } else { + abortFallbackLoop = true; + } + } + } + + this.lookUpBuffer[posInBuffer] = address; + this.lookUpBuffer[posInBuffer + 1] = bucketZoomStep; } this.lookUpTexture.update(this.lookUpBuffer, 0, 0, lookUpBufferWidth, lookUpBufferWidth); @@ -281,12 +327,7 @@ export default class TextureBucketManager { _getBucketIndex(bucket: DataBucket): number { const bucketPosition = bucket.zoomedAddress; - const renderingZoomStep = this.currentAnchorPoint[3]; - const bucketZoomStep = bucketPosition[3]; - const zoomDiff = bucketZoomStep - renderingZoomStep; - const isFallbackBucket = zoomDiff > 0; - - const anchorPoint = isFallbackBucket ? this.fallbackAnchorPoint : this.currentAnchorPoint; + const anchorPoint = this.currentAnchorPoint; const x = bucketPosition[0] - anchorPoint[0]; const y = bucketPosition[1] - anchorPoint[1]; @@ -296,20 +337,12 @@ export default class TextureBucketManager { // if (y < 0) console.warn("y should be greater than 0. is currently:", y); // if (z < 0) console.warn("z should be greater than 0. is currently:", z); - // Even though, addressSpaceDimensions might be different in the fallback case, - // it's safe to assume that the values would only be smaller (since - // fallback data doesn't require more buckets than non-fallback). - // Consequently, these values should be fine to address buckets. - const [sx, sy, sz] = addressSpaceDimensions.normal; - const [_sx, _sy] = isFallbackBucket - ? addressSpaceDimensions.fallback - : addressSpaceDimensions.normal; + const [sx, sy] = addressSpaceDimensions; // prettier-ignore return ( - sx * sy * sz * zoomDiff + - _sx * _sy * z + - _sx * y + + sx * sy * z + + sx * y + x ); } diff --git a/frontend/javascripts/oxalis/shaders/coords.glsl.js b/frontend/javascripts/oxalis/shaders/coords.glsl.js index b85a01cfd0..e7c3cc57bc 100644 --- a/frontend/javascripts/oxalis/shaders/coords.glsl.js +++ b/frontend/javascripts/oxalis/shaders/coords.glsl.js @@ -20,25 +20,28 @@ export const getResolution: ShaderModule = { `, }; +export const getResolutionFactors: ShaderModule = { + requirements: [getResolution], + code: ` + vec3 getResolutionFactors(float zoomStepA, float zoomStepB) { + return getResolution(zoomStepA) / getResolution(zoomStepB); + } + `, +}; + export const getRelativeCoords: ShaderModule = { requirements: [getResolution], code: ` vec3 getRelativeCoords(vec3 worldCoordUVW, float usedZoomStep) { - float zoomStepDiff = usedZoomStep - zoomStep; - bool useFallback = zoomStepDiff > 0.0; - vec3 usedAnchorPoint = useFallback ? fallbackAnchorPoint : anchorPoint; - vec3 usedAnchorPointUVW = transDim(usedAnchorPoint); - vec3 resolution = getResolution(usedZoomStep); - float zoomValue = pow(2.0, usedZoomStep); - vec3 resolutionUVW = transDim(resolution); + + vec3 anchorPointUVW = transDim(anchorPoint); vec3 anchorPointAsGlobalPositionUVW = - usedAnchorPointUVW * resolutionUVW * bucketWidth; + anchorPointUVW * resolutionUVW * bucketWidth; vec3 relativeCoords = (worldCoordUVW - anchorPointAsGlobalPositionUVW) / resolutionUVW; vec3 coords = transDim(relativeCoords); - return coords; } `, diff --git a/frontend/javascripts/oxalis/shaders/filtering.glsl.js b/frontend/javascripts/oxalis/shaders/filtering.glsl.js index 25fc56ba51..e41d7ddd2b 100644 --- a/frontend/javascripts/oxalis/shaders/filtering.glsl.js +++ b/frontend/javascripts/oxalis/shaders/filtering.glsl.js @@ -10,17 +10,16 @@ export const getBilinearColorFor: ShaderModule = { float layerIndex, float d_texture_width, float packingDegree, - vec3 coords, - float isFallback + vec3 coords ) { coords = coords + transDim(vec3(-0.5, -0.5, 0.0)); vec2 bifilteringParams = transDim((coords - floor(coords))).xy; coords = floor(coords); - vec4 a = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, isFallback); - vec4 b = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 0)), isFallback); - vec4 c = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 0)), isFallback); - vec4 d = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 0)), isFallback); + vec4 a = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords); + vec4 b = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 0))); + vec4 c = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 0))); + vec4 d = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 0))); if (a.a < 0.0 || b.a < 0.0 || c.a < 0.0 || d.a < 0.0) { // We need to check all four colors for a negative parts, because there will be black // lines at the borders otherwise (black gets mixed with data) @@ -43,22 +42,21 @@ export const getTrilinearColorFor: ShaderModule = { float layerIndex, float d_texture_width, float packingDegree, - vec3 coords, - float isFallback + vec3 coords ) { coords = coords + transDim(vec3(-0.5, -0.5, 0.0)); vec3 bifilteringParams = transDim((coords - floor(coords))).xyz; coords = floor(coords); - vec4 a = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, isFallback); - vec4 b = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 0)), isFallback); - vec4 c = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 0)), isFallback); - vec4 d = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 0)), isFallback); + vec4 a = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords); + vec4 b = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 0))); + vec4 c = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 0))); + vec4 d = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 0))); - vec4 a2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 0, 1)), isFallback); - vec4 b2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 1)), isFallback); - vec4 c2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 1)), isFallback); - vec4 d2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 1)), isFallback); + vec4 a2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 0, 1))); + vec4 b2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 0, 1))); + vec4 c2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(0, 1, 1))); + vec4 d2 = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords + transDim(vec3(1, 1, 1))); if (a.a < 0.0 || b.a < 0.0 || c.a < 0.0 || d.a < 0.0 || a2.a < 0.0 || b2.a < 0.0 || c2.a < 0.0 || d2.a < 0.0) { @@ -89,19 +87,18 @@ const getMaybeFilteredColor: ShaderModule = { float layerIndex, float d_texture_width, float packingDegree, - vec3 coords, - bool suppressBilinearFiltering, - float isFallback + vec3 worldPositionUVW, + bool suppressBilinearFiltering ) { vec4 color; if (!suppressBilinearFiltering && useBilinearFiltering) { <% if (isOrthogonal) { %> - color = getBilinearColorFor(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, isFallback); + color = getBilinearColorFor(lookUpTexture, layerIndex, d_texture_width, packingDegree, worldPositionUVW); <% } else { %> - color = getTrilinearColorFor(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, isFallback); + color = getTrilinearColorFor(lookUpTexture, layerIndex, d_texture_width, packingDegree, worldPositionUVW); <% } %> } else { - color = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, isFallback); + color = getColorForCoords(lookUpTexture, layerIndex, d_texture_width, packingDegree, worldPositionUVW); } return color; } @@ -116,17 +113,12 @@ export const getMaybeFilteredColorOrFallback: ShaderModule = { float layerIndex, float d_texture_width, float packingDegree, - vec3 coords, - vec3 fallbackCoords, - bool hasFallback, + vec3 worldPositionUVW, bool suppressBilinearFiltering, vec4 fallbackColor ) { - vec4 color = getMaybeFilteredColor(lookUpTexture, layerIndex, d_texture_width, packingDegree, coords, suppressBilinearFiltering, 0.0); + vec4 color = getMaybeFilteredColor(lookUpTexture, layerIndex, d_texture_width, packingDegree, worldPositionUVW, suppressBilinearFiltering); - if (color.a < 0.0 && hasFallback) { - color = getMaybeFilteredColor(lookUpTexture, layerIndex, d_texture_width, packingDegree, fallbackCoords, suppressBilinearFiltering, 1.0); - } if (color.a < 0.0) { // Render gray for not-yet-existing data color = fallbackColor; diff --git a/frontend/javascripts/oxalis/shaders/main_data_fragment.glsl.js b/frontend/javascripts/oxalis/shaders/main_data_fragment.glsl.js index 296ccda4a9..5617e5907c 100644 --- a/frontend/javascripts/oxalis/shaders/main_data_fragment.glsl.js +++ b/frontend/javascripts/oxalis/shaders/main_data_fragment.glsl.js @@ -5,7 +5,6 @@ import { MAPPING_TEXTURE_WIDTH, MAPPING_COLOR_TEXTURE_WIDTH, } from "oxalis/model/bucket_data_handling/mappings"; -import { floatsPerLookUpEntry } from "oxalis/model/bucket_data_handling/texture_bucket_manager"; import constants, { ViewModeValuesIndices, OrthoViewIndices, @@ -90,7 +89,6 @@ uniform vec3 bboxMin; uniform vec3 bboxMax; uniform vec3 globalPosition; uniform vec3 anchorPoint; -uniform vec3 fallbackAnchorPoint; uniform float zoomStep; uniform float zoomValue; uniform vec3 uvw; @@ -100,7 +98,6 @@ uniform bool isMouseInCanvas; uniform float brushSizeInPixel; uniform float planeID; uniform vec3 addressSpaceDimensions; -uniform vec3 addressSpaceDimensionsFallback; varying vec4 worldCoord; varying vec4 modelCoord; @@ -109,7 +106,6 @@ varying mat4 savedModelMatrix; const float bucketWidth = <%= bucketWidth %>; const float bucketSize = <%= bucketSize %>; const float l_texture_width = <%= l_texture_width %>; -const float floatsPerLookUpEntry = <%= floatsPerLookUpEntry %>; // For some reason, taking the dataset scale from the uniform results is imprecise // rendering of the brush circle (and issues in the arbitrary modes). That's why it @@ -140,27 +136,19 @@ void main() { gl_FragColor = vec4(0.0); return; } - vec3 coords = getRelativeCoords(worldCoordUVW, zoomStep); + vec3 relativeCoords = getRelativeCoords(worldCoordUVW, zoomStep); - vec3 bucketPosition = div(floor(coords), bucketWidth); + vec3 bucketPosition = div(floor(relativeCoords), bucketWidth); if (renderBucketIndices) { gl_FragColor = vec4(bucketPosition, zoomStep) / 255.; - // gl_FragColor = vec4(0.5, 1.0, 1.0, 1.0); return; } - vec3 offsetInBucket = mod(floor(coords), bucketWidth); <% if (hasSegmentation) { %> - float segmentationFallbackZoomStep = min(<%= segmentationName %>_maxZoomStep, zoomStep + 1.0); - bool segmentationHasFallback = segmentationFallbackZoomStep > zoomStep; - vec3 segmentationFallbackCoords = floor(getRelativeCoords(worldCoordUVW, segmentationFallbackZoomStep)); - - vec4 id = getSegmentationId(coords, segmentationFallbackCoords, segmentationHasFallback); + vec4 id = getSegmentationId(worldCoordUVW); vec3 flooredMousePosUVW = transDim(floor(globalMousePosition)); - vec3 mousePosCoords = getRelativeCoords(flooredMousePosUVW, zoomStep); - - vec4 cellIdUnderMouse = getSegmentationId(mousePosCoords, segmentationFallbackCoords, false); + vec4 cellIdUnderMouse = getSegmentationId(flooredMousePosUVW); <% } %> // Get Color Value(s) @@ -168,12 +156,10 @@ void main() { vec3 color_value = vec3(0.0); float fallbackZoomStep; bool hasFallback; - vec3 fallbackCoords; <% _.each(colorLayerNames, function(name, layerIndex){ %> fallbackZoomStep = min(<%= name %>_maxZoomStep, zoomStep + 1.0); hasFallback = fallbackZoomStep > zoomStep; - fallbackCoords = floor(getRelativeCoords(worldCoordUVW, fallbackZoomStep)); // Get grayscale value for <%= name %> color_value = getMaybeFilteredColorOrFallback( @@ -181,9 +167,7 @@ void main() { <%= formatNumberAsGLSLFloat(layerIndex) %>, <%= name %>_data_texture_width, <%= isRgbLayerLookup[name] ? "1.0" : "4.0" %>, // RGB data cannot be packed, gray scale data is always packed into rgba channels - coords, - fallbackCoords, - hasFallback, + worldCoordUVW, false, fallbackGray ).xyz; @@ -236,7 +220,6 @@ void main() { formatNumberAsGLSLFloat, formatVector3AsVec3: vector3 => `vec3(${vector3.map(formatNumberAsGLSLFloat).join(", ")})`, brushToolIndex: formatNumberAsGLSLFloat(volumeToolEnumToIndex(VolumeToolEnum.BRUSH)), - floatsPerLookUpEntry: formatNumberAsGLSLFloat(floatsPerLookUpEntry), OrthoViewIndices: _.mapValues(OrthoViewIndices, formatNumberAsGLSLFloat), }); } diff --git a/frontend/javascripts/oxalis/shaders/segmentation.glsl.js b/frontend/javascripts/oxalis/shaders/segmentation.glsl.js index 5263adac36..6ca1ab06e8 100644 --- a/frontend/javascripts/oxalis/shaders/segmentation.glsl.js +++ b/frontend/javascripts/oxalis/shaders/segmentation.glsl.js @@ -63,16 +63,14 @@ export const getBrushOverlay: ShaderModule = { export const getSegmentationId: ShaderModule = { requirements: [binarySearchIndex, getRgbaAtIndex], code: ` - vec4 getSegmentationId(vec3 coords, vec3 fallbackCoords, bool hasFallback) { + vec4 getSegmentationId(vec3 worldPositionUVW) { vec4 volume_color = getMaybeFilteredColorOrFallback( <%= segmentationName %>_lookup_texture, <%= formatNumberAsGLSLFloat(segmentationLayerIndex) %>, <%= segmentationName %>_data_texture_width, <%= segmentationPackingDegree %>, - coords, - fallbackCoords, - hasFallback, + worldPositionUVW, true, // Don't use bilinear filtering for volume data vec4(0.0, 0.0, 0.0, 0.0) ); diff --git a/frontend/javascripts/oxalis/shaders/texture_access.glsl.js b/frontend/javascripts/oxalis/shaders/texture_access.glsl.js index 79ed3b23e5..4f96df5cdf 100644 --- a/frontend/javascripts/oxalis/shaders/texture_access.glsl.js +++ b/frontend/javascripts/oxalis/shaders/texture_access.glsl.js @@ -1,5 +1,7 @@ // @flow +import { getResolutionFactors, getRelativeCoords } from "oxalis/shaders/coords.glsl"; + import type { ShaderModule } from "./shader_module_system"; export const linearizeVec3ToIndex: ShaderModule = { @@ -97,37 +99,63 @@ export const getRgbaAtXYIndex: ShaderModule = { `, }; -const getColorFor: ShaderModule = { +export const getColorForCoords: ShaderModule = { requirements: [ linearizeVec3ToIndex, linearizeVec3ToIndexWithMod, getRgbaAtIndex, getRgbaAtXYIndex, + getRelativeCoords, + getResolutionFactors, ], code: ` - vec4 getColorFor( + vec4 getColorForCoords( sampler2D lookUpTexture, float layerIndex, float d_texture_width, float packingDegree, - vec3 bucketPosition, - vec3 offsetInBucket, - float isFallback + vec3 worldPositionUVW ) { - float bucketIdx = linearizeVec3ToIndex(bucketPosition, isFallback > 0.0 ? addressSpaceDimensionsFallback : addressSpaceDimensions); + vec3 coords = floor(getRelativeCoords(worldPositionUVW, zoomStep)); + vec3 relativeBucketPosition = div(coords, bucketWidth); + vec3 offsetInBucket = mod(coords, bucketWidth); - // If we are making a fallback lookup, the lookup area we are interested in starts at - // volumeOf(addressSpaceDimensions). If isFallback is true, we use that offset. Otherwise, the offset is 0. - float fallbackOffset = isFallback * addressSpaceDimensions.x * addressSpaceDimensions.y * addressSpaceDimensions.z; - float bucketIdxInTexture = - bucketIdx * floatsPerLookUpEntry - + fallbackOffset; + float bucketIdx = linearizeVec3ToIndex(relativeBucketPosition, addressSpaceDimensions); - float bucketAddress = getRgbaAtIndex( + vec2 bucketAddressWithZoomStep = getRgbaAtIndex( lookUpTexture, l_texture_width, - bucketIdxInTexture - ).x; + bucketIdx + ).ra; + + float bucketAddress = bucketAddressWithZoomStep.x; + float renderedZoomStep = bucketAddressWithZoomStep.y; + + if (renderedZoomStep != zoomStep) { + /* We already know which fallback bucket we have to look into. However, + * for 8 mag-1 buckets, there is usually one fallback bucket in mag-2. + * Therefore, depending on the actual mag-1 bucket, we have to look into + * different sub-volumes of the one fallback bucket. This is calculated as + * the subVolumeIndex. + * Then, we adapt the look up position *within* the bucket. + * + * Example Scenario (let's consider only the x axis): + * If we are in the [4, _, _, 0]-bucket, we have to look into the **first** half + * of the [2, _, _, 1]-bucket. + * If we are in the [5, _, _, 0]-bucket, we have to look into the **second** half + * of the [2, _, _, 1]-bucket. + * We can determine which "half" (subVolumeIndex) is relevant by doing a modulo operation + * with the resolution factor. A typical resolution factor is 2. + */ + + vec3 magnificationFactors = getResolutionFactors(renderedZoomStep, zoomStep); + vec3 worldBucketPosition = relativeBucketPosition + anchorPoint; + vec3 subVolumeIndex = mod(worldBucketPosition, magnificationFactors); + offsetInBucket = floor( + (offsetInBucket + vec3(bucketWidth) * subVolumeIndex) + / magnificationFactors + ); + } if (bucketAddress == -2.0) { // The bucket is out of bounds. Render black @@ -135,7 +163,7 @@ const getColorFor: ShaderModule = { // since the approximate implementation of the bucket picker missed the bucket. // We simply handle this case as if the bucket was not yet loaded which means // that fallback data is loaded. - // The downside is that data which does exist, will be rendered gray instead of black. + // The downside is that data which does not exist, will be rendered gray instead of black. // Issue to track progress: #3446 float alpha = isFlightMode() ? -1.0 : 0.0; return vec4(0.0, 0.0, 0.0, alpha); @@ -193,31 +221,3 @@ const getColorFor: ShaderModule = { } `, }; - -export const getColorForCoords: ShaderModule = { - requirements: [getColorFor], - code: ` - vec4 getColorForCoords( - sampler2D lookUpTexture, - float layerIndex, - float d_texture_width, - float packingDegree, - vec3 coords, - float isFallback - ) { - coords = floor(coords); - vec3 bucketPosition = div(coords, bucketWidth); - vec3 offsetInBucket = mod(coords, bucketWidth); - - return getColorFor( - lookUpTexture, - layerIndex, - d_texture_width, - packingDegree, - bucketPosition, - offsetInBucket, - isFallback - ); - } - `, -}; diff --git a/frontend/javascripts/oxalis/store.js b/frontend/javascripts/oxalis/store.js index bef6da4c27..83803c61ff 100644 --- a/frontend/javascripts/oxalis/store.js +++ b/frontend/javascripts/oxalis/store.js @@ -215,6 +215,8 @@ export type DatasetLayerConfiguration = {| +alpha: number, |}; +export type LoadingStrategy = "BEST_QUALITY_FIRST" | "PROGRESSIVE_QUALITY"; + export type DatasetConfiguration = {| +fourBit: boolean, +interpolation: boolean, @@ -229,6 +231,7 @@ export type DatasetConfiguration = {| +zoom?: number, +rotation?: Vector3, +renderMissingDataBlack: boolean, + +loadingStrategy: LoadingStrategy, |}; export type UserConfiguration = {| @@ -412,10 +415,11 @@ const initialAnnotationInfo = { export const defaultState: OxalisState = { datasetConfiguration: { - fourBit: true, + fourBit: false, interpolation: false, layers: {}, quality: 0, + loadingStrategy: "PROGRESSIVE_QUALITY", segmentationOpacity: 20, highlightHoveredCellId: true, renderIsosurfaces: false, diff --git a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js index bc35c9b49e..3b2a469f8e 100644 --- a/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js +++ b/frontend/javascripts/oxalis/view/layouting/tracing_layout_view.js @@ -186,7 +186,7 @@ class TracingLayoutView extends React.PureComponent { collapsed={this.state.isSettingsCollapsed} collapsedWidth={0} width={350} - style={{ zIndex: 100 }} + style={{ zIndex: 100, marginRight: this.state.isSettingsCollapsed ? 0 : 8 }} > {/* Don't render SettingsView if it's hidden to improve performance */} {!this.state.isSettingsCollapsed ? : null} diff --git a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js index 648d2e72e7..2055a70c36 100644 --- a/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/dataset_settings_view.js @@ -173,14 +173,45 @@ class DatasetSettings extends React.PureComponent { ); return ( - + {colorSettings} {this.props.hasSegmentation ? this.getSegmentationPanel() : null} - + + + + + + + + {settings.loadingStrategy}{" "} + + + + + } + value={this.props.datasetConfiguration.loadingStrategy} + onChange={_.partial(this.props.onChange, "loadingStrategy")} + > + + + + {settings.fourBit}{" "} + + + + + } value={this.props.datasetConfiguration.fourBit} onChange={_.partial(this.props.onChange, "fourBit")} /> @@ -191,20 +222,11 @@ class DatasetSettings extends React.PureComponent { onChange={_.partial(this.props.onChange, "interpolation")} /> )} - - - - - {settings.renderMissingDataBlack}{" "} - + diff --git a/frontend/javascripts/oxalis/view/settings/setting_input_views.js b/frontend/javascripts/oxalis/view/settings/setting_input_views.js index 8f92761d23..ebe9563b75 100644 --- a/frontend/javascripts/oxalis/view/settings/setting_input_views.js +++ b/frontend/javascripts/oxalis/view/settings/setting_input_views.js @@ -31,17 +31,17 @@ export class NumberSliderSetting extends React.PureComponent - + - + render() { const { label, roundTo, value, min, max, disabled } = this.props; return ( - - + + @@ -120,11 +120,11 @@ export class LogSliderSetting extends React.PureComponent disabled={disabled} /> - + { render() { const { label, onChange, value } = this.props; return ( - - + + - + @@ -178,12 +178,13 @@ export class NumberInputSetting extends React.PureComponent - + + - + + @@ -328,12 +329,29 @@ export class ColorSetting extends React.PureComponent { render() { return ( - - + + - - + +
+ +
); @@ -342,8 +360,8 @@ export class ColorSetting extends React.PureComponent { type DropdownSettingProps = { onChange: (value: number) => void, - label: string, - value: number, + label: React.Node | string, + value: number | string, children?: Array, }; @@ -355,7 +373,7 @@ export class DropdownSetting extends React.PureComponent { render() { const { onChange, label, value, children } = this.props; return ( - + @@ -365,6 +383,7 @@ export class DropdownSetting extends React.PureComponent { value={value.toString()} defaultValue={value.toString()} size="small" + dropdownMatchSelectWidth={false} > {children} diff --git a/frontend/javascripts/oxalis/view/settings/user_settings_view.js b/frontend/javascripts/oxalis/view/settings/user_settings_view.js index 41896df4b2..4838987902 100644 --- a/frontend/javascripts/oxalis/view/settings/user_settings_view.js +++ b/frontend/javascripts/oxalis/view/settings/user_settings_view.js @@ -301,7 +301,7 @@ class UserSettingsView extends PureComponent { ); return ( - + { test("Flycam Accessors should calculate the request log zoom step (2/3)", t => { const state = _.cloneDeep(initialState); + // $FlowFixMe state.datasetConfiguration.quality = 1; t.is(accessors.getRequestLogZoomStep(state), 1); }); test("Flycam Accessors should calculate the request log zoom step (3/3)", t => { const state = _.cloneDeep(initialState); + // $FlowFixMe state.datasetConfiguration.quality = 1; + // $FlowFixMe state.flycam.zoomStep = 8; t.is(accessors.getRequestLogZoomStep(state), 4); }); -test("Flycam Accessors should calculate the texture scaling factor (1/2)", t => { - const texturePosition = accessors.getTextureScalingFactor(initialState); - t.deepEqual(texturePosition, 1.3); -}); - -test("Flycam Accessors should calculate the texture scaling factor (2/2)", t => { - const state = _.cloneDeep(initialState); - state.datasetConfiguration.quality = 1; - state.flycam.zoomStep = 8.6; - - const texturePosition = accessors.getTextureScalingFactor(state); - t.deepEqual(texturePosition, 0.5375); -}); - test.only("Flycam Accessors should calculate appropriate zoom factors for datasets with many magnifications.", t => { const scale = [4, 4, 35]; const resolutions = [ @@ -99,6 +90,7 @@ test.only("Flycam Accessors should calculate appropriate zoom factors for datase const maximumZoomPerResolution = accessors._getMaximumZoomForAllResolutions( constants.MODE_PLANE_TRACING, + "BEST_QUALITY_FIRST", scale, resolutions, rects, @@ -113,13 +105,13 @@ test.only("Flycam Accessors should calculate appropriate zoom factors for datase 4.594972986357223, 7.4002499442581735, 15.86309297171495, - 30.91268053287076, - 60.240069161242396, - 117.39085287969576, - 251.6377186292722, - 490.3707252978515, - 955.5938177273264, - 1862.1820132595253, + 34.00394858615784, + 66.26407607736664, + 129.12993816766533, + 276.80149049219943, + 539.4077978276367, + 1051.1531995000591, + 2048.400214585478, 4390.927778387033, ]; diff --git a/frontend/javascripts/test/model/texture_bucket_manager.spec.js b/frontend/javascripts/test/model/texture_bucket_manager.spec.js index 67bd04340e..869b188add 100644 --- a/frontend/javascripts/test/model/texture_bucket_manager.spec.js +++ b/frontend/javascripts/test/model/texture_bucket_manager.spec.js @@ -1,9 +1,17 @@ // @flow - /* eslint import/no-extraneous-dependencies: ["error", {"peerDependencies": true}] */ + +import * as THREE from "three"; import mock from "mock-require"; import test from "ava"; +const formatToChannelCount = new Map([ + [THREE.LuminanceFormat, 1], + [THREE.LuminanceAlphaFormat, 2], + [THREE.RGBFormat, 3], + [THREE.RGBAFormat, 4], +]); + global.performance = { now: () => Date.now(), }; @@ -14,6 +22,11 @@ mock( texture: Uint8Array; width: number; height: number; + channelCount: number; + + constructor(_width, _height, format) { + this.channelCount = formatToChannelCount.get(format) || 0; + } update(src, x, y, _width, _height) { this.texture.set(src, y * this.width + x); @@ -22,7 +35,7 @@ mock( setRenderer() {} setSize(width, height) { - this.texture = new Uint8Array(width * height); + this.texture = new Uint8Array(width * height * this.channelCount); this.width = width; this.height = height; } @@ -37,7 +50,7 @@ const temporalBucketManagerMock = { addBucket: () => {}, }; -const { default: TextureBucketManager } = mock.reRequire( +const { default: TextureBucketManager, channelCountForLookupBuffer } = mock.reRequire( "oxalis/model/bucket_data_handling/texture_bucket_manager", ); const { DataBucket } = mock.reRequire("oxalis/model/bucket_data_handling/bucket"); @@ -51,8 +64,8 @@ const buildBucket = (zoomedAddress, firstByte) => { return bucket; }; -const setActiveBucketsAndWait = (tbm, activeBuckets, anchorPoint, fallbackAnchorPoint) => { - tbm.setActiveBuckets(activeBuckets, anchorPoint, fallbackAnchorPoint); +const setActiveBucketsAndWait = (tbm, activeBuckets, anchorPoint) => { + tbm.setActiveBuckets(activeBuckets, anchorPoint); // Depending on timing, processWriterQueue has to be called n times in the slowest case activeBuckets.forEach(() => tbm.processWriterQueue()); tbm._refreshLookUpBuffer(); @@ -60,7 +73,8 @@ const setActiveBucketsAndWait = (tbm, activeBuckets, anchorPoint, fallbackAnchor const expectBucket = (t, tbm, bucket, expectedFirstByte) => { const bucketIdx = tbm._getBucketIndex(bucket); - const bucketLocation = tbm.getPackedBucketSize() * tbm.lookUpBuffer[bucketIdx]; + const bucketLocation = + tbm.getPackedBucketSize() * tbm.lookUpBuffer[channelCountForLookupBuffer * bucketIdx]; t.is(tbm.dataTextures[0].texture[bucketLocation], expectedFirstByte); }; @@ -74,7 +88,7 @@ test("TextureBucketManager: basic functionality", t => { buildBucket([1, 2, 1, 0], 102), ]; - setActiveBucketsAndWait(tbm, activeBuckets, [1, 1, 1, 0], [0, 0, 0, 1]); + setActiveBucketsAndWait(tbm, activeBuckets, [1, 1, 1, 0]); expectBucket(t, tbm, activeBuckets[0], 100); expectBucket(t, tbm, activeBuckets[1], 101); @@ -94,8 +108,8 @@ test("TextureBucketManager: changing active buckets", t => { buildBucket([1, 1, 0, 0], 202), ]; - setActiveBucketsAndWait(tbm, activeBuckets.slice(0, 3), [0, 0, 0, 0], [0, 0, 0, 1]); - setActiveBucketsAndWait(tbm, activeBuckets.slice(3, 6), [1, 0, 0, 0], [0, 0, 0, 1]); + setActiveBucketsAndWait(tbm, activeBuckets.slice(0, 3), [0, 0, 0, 0]); + setActiveBucketsAndWait(tbm, activeBuckets.slice(3, 6), [1, 0, 0, 0]); expectBucket(t, tbm, activeBuckets[3], 200); expectBucket(t, tbm, activeBuckets[4], 201); diff --git a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js index fca169ff59..ff483daa3e 100644 --- a/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js +++ b/frontend/javascripts/test/puppeteer/dataset_rendering.screenshot.js @@ -90,6 +90,7 @@ const datasetConfigOverrides: { [key: string]: DatasetConfiguration } = { highlightHoveredCellId: true, renderIsosurfaces: false, renderMissingDataBlack: false, + loadingStrategy: "BEST_QUALITY_FIRST", }, }; diff --git a/frontend/stylesheets/antd_overwrites.less b/frontend/stylesheets/antd_overwrites.less index 88e23885ad..8dd56ff650 100644 --- a/frontend/stylesheets/antd_overwrites.less +++ b/frontend/stylesheets/antd_overwrites.less @@ -82,6 +82,12 @@ label.ant-checkbox-wrapper { line-height: 1.5; } +.ant-layout-sider { + // This solves problems with the settings sider, which sometimes doesn't appear + // when opening it. + transition: none; +} + // Remove Chinese Quote font, which is messing up our quotes .ant-alert { font-family: "Monospaced Number", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,