⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 278284 in webkit


Ignore:
Timestamp:
May 31, 2021, 5:03:03 PM (5 years ago)
Author:
Chris Dumez
Message:

Fix thread safety issues in PannerNode
https://bugs.webkit.org/show_bug.cgi?id=226455

Reviewed by Darin Adler.

Adopt thread safety annotations in PannerNode and fix bugs found by clang.
In particular, the following issues were fixed:

  • tailTime() / latencyTime() were accessing m_panner on the audio thread without locking even though m_panner gets modified on the main thread.
  • process() was accessing panningModel and m_panner before locking, on the audio thread, even though those get modified on the main thread.
  • processOnlyAudioParams() was failing to grab the process lock.
  • requiresTailProcessing() may get called on the audio thread and was failing to grab the processLock before accessing m_panner, which gets modified on the main thread.
  • Modules/webaudio/PannerNode.cpp:

(WebCore::PannerNode::create):
(WebCore::PannerNode::PannerNode):
(WebCore::PannerNode::process):
(WebCore::PannerNode::processOnlyAudioParams):
(WebCore::PannerNode::setPanningModelForBindings):
(WebCore::PannerNode::setDistanceModelForBindings):
(WebCore::PannerNode::setRefDistanceForBindings):
(WebCore::PannerNode::setMaxDistanceForBindings):
(WebCore::PannerNode::setRolloffFactorForBindings):
(WebCore::PannerNode::setConeOuterGainForBindings):
(WebCore::PannerNode::setConeOuterAngleForBindings):
(WebCore::PannerNode::setConeInnerAngleForBindings):
(WebCore::PannerNode::requiresTailProcessing const):
(WebCore::PannerNode::tailTime const):
(WebCore::PannerNode::latencyTime const):

  • Modules/webaudio/PannerNode.h:
  • Modules/webaudio/PannerNode.idl:
  • platform/audio/Distance.h:
Location:
trunk/Source/WebCore
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r278283 r278284  
     12021-05-31  Chris Dumez  <cdumez@apple.com>
     2
     3        Fix thread safety issues in PannerNode
     4        https://bugs.webkit.org/show_bug.cgi?id=226455
     5
     6        Reviewed by Darin Adler.
     7
     8        Adopt thread safety annotations in PannerNode and fix bugs found by clang.
     9        In particular, the following issues were fixed:
     10        - tailTime() / latencyTime() were accessing m_panner on the audio thread without locking
     11          even though m_panner gets modified on the main thread.
     12        - process() was accessing panningModel and m_panner before locking, on the audio thread,
     13          even though those get modified on the main thread.
     14        - processOnlyAudioParams() was failing to grab the process lock.
     15        - requiresTailProcessing() may get called on the audio thread and was failing to grab
     16          the processLock before accessing m_panner, which gets modified on the main thread.
     17
     18        * Modules/webaudio/PannerNode.cpp:
     19        (WebCore::PannerNode::create):
     20        (WebCore::PannerNode::PannerNode):
     21        (WebCore::PannerNode::process):
     22        (WebCore::PannerNode::processOnlyAudioParams):
     23        (WebCore::PannerNode::setPanningModelForBindings):
     24        (WebCore::PannerNode::setDistanceModelForBindings):
     25        (WebCore::PannerNode::setRefDistanceForBindings):
     26        (WebCore::PannerNode::setMaxDistanceForBindings):
     27        (WebCore::PannerNode::setRolloffFactorForBindings):
     28        (WebCore::PannerNode::setConeOuterGainForBindings):
     29        (WebCore::PannerNode::setConeOuterAngleForBindings):
     30        (WebCore::PannerNode::setConeInnerAngleForBindings):
     31        (WebCore::PannerNode::requiresTailProcessing const):
     32        (WebCore::PannerNode::tailTime const):
     33        (WebCore::PannerNode::latencyTime const):
     34        * Modules/webaudio/PannerNode.h:
     35        * Modules/webaudio/PannerNode.idl:
     36        * platform/audio/Distance.h:
     37
    1382021-05-31  Adrian Perez de Castro  <aperez@igalia.com>
    239
  • trunk/Source/WebCore/Modules/webaudio/PannerNode.cpp

    r277932 r278284  
    5959        return result.releaseException();
    6060
    61     result = panner->setMaxDistance(options.maxDistance);
    62     if (result.hasException())
    63         return result.releaseException();
    64 
    65     result = panner->setRefDistance(options.refDistance);
    66     if (result.hasException())
    67         return result.releaseException();
    68 
    69     result = panner->setRolloffFactor(options.rolloffFactor);
    70     if (result.hasException())
    71         return result.releaseException();
    72 
    73     result = panner->setConeOuterGain(options.coneOuterGain);
     61    result = panner->setMaxDistanceForBindings(options.maxDistance);
     62    if (result.hasException())
     63        return result.releaseException();
     64
     65    result = panner->setRefDistanceForBindings(options.refDistance);
     66    if (result.hasException())
     67        return result.releaseException();
     68
     69    result = panner->setRolloffFactorForBindings(options.rolloffFactor);
     70    if (result.hasException())
     71        return result.releaseException();
     72
     73    result = panner->setConeOuterGainForBindings(options.coneOuterGain);
    7474    if (result.hasException())
    7575        return result.releaseException();
     
    8080PannerNode::PannerNode(BaseAudioContext& context, const PannerOptions& options)
    8181    : AudioNode(context, NodeTypePanner)
     82    // Load the HRTF database asynchronously so we don't block the Javascript thread while creating the HRTF database.
     83    , m_hrtfDatabaseLoader(HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(context.sampleRate()))
    8284    , m_panningModel(options.panningModel)
     85    , m_panner(Panner::create(m_panningModel, sampleRate(), m_hrtfDatabaseLoader.ptr()))
    8386    , m_positionX(AudioParam::create(context, "positionX"_s, options.positionX, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
    8487    , m_positionY(AudioParam::create(context, "positionY"_s, options.positionY, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
     
    8790    , m_orientationY(AudioParam::create(context, "orientationY"_s, options.orientationY, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
    8891    , m_orientationZ(AudioParam::create(context, "orientationZ"_s, options.orientationZ, -FLT_MAX, FLT_MAX, AutomationRate::ARate))
    89     // Load the HRTF database asynchronously so we don't block the Javascript thread while creating the HRTF database.
    90     , m_hrtfDatabaseLoader(HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(context.sampleRate()))
    91 {
    92     setDistanceModel(options.distanceModel);
    93     setConeInnerAngle(options.coneInnerAngle);
    94     setConeOuterAngle(options.coneOuterAngle);
     92{
     93    setDistanceModelForBindings(options.distanceModel);
     94    setConeInnerAngleForBindings(options.coneInnerAngle);
     95    setConeOuterAngleForBindings(options.coneOuterAngle);
    9596
    9697    addInput();
     
    109110    AudioBus* destination = output(0)->bus();
    110111
    111     if (!isInitialized() || !input(0)->isConnected() || !m_panner.get()) {
     112    if (!isInitialized() || !input(0)->isConnected()) {
    112113        destination->zero();
    113114        return;
     
    120121    }
    121122
    122     // HRTFDatabase should be loaded before proceeding for offline audio context when panningModel() is "HRTF".
    123     if (panningModel() == PanningModelType::HRTF && !m_hrtfDatabaseLoader->isLoaded()) {
     123    // The audio thread can't block on this lock, so we use tryLock() instead.
     124    if (!m_processLock.tryLock()) {
     125        // Too bad - tryLock() failed. We must be in the middle of changing the panner.
     126        destination->zero();
     127        return;
     128    }
     129    Locker locker { AdoptLock, m_processLock };
     130
     131    if (!m_panner) {
     132        destination->zero();
     133        return;
     134    }
     135
     136    // HRTFDatabase should be loaded before proceeding for offline audio context when m_panningModel is "HRTF".
     137    if (m_panningModel == PanningModelType::HRTF && !m_hrtfDatabaseLoader->isLoaded()) {
    124138        if (context().isOfflineContext())
    125139            m_hrtfDatabaseLoader->waitForLoaderThreadCompletion();
     
    130144    }
    131145
    132     // The audio thread can't block on this lock, so we use tryLock() instead.
    133     if (!m_processLock.tryLock()) {
    134         // Too bad - tryLock() failed. We must be in the middle of changing the panner.
    135         destination->zero();
    136         return;
    137     }
    138     Locker locker { AdoptLock, m_processLock };
    139 
    140146    if ((hasSampleAccurateValues() || listener().hasSampleAccurateValues()) && (shouldUseARate() || listener().shouldUseARate())) {
    141147        processSampleAccurateValues(destination, source, framesToProcess);
     
    158164void PannerNode::processOnlyAudioParams(size_t framesToProcess)
    159165{
     166    ASSERT(context().isAudioThread());
     167    if (!m_processLock.tryLock())
     168        return;
     169
     170    Locker locker { AdoptLock, m_processLock };
    160171    float values[AudioUtilities::renderQuantumSize];
    161172    ASSERT(framesToProcess <= AudioUtilities::renderQuantumSize);
     
    246257}
    247258
    248 void PannerNode::initialize()
    249 {
    250     if (isInitialized())
    251         return;
    252 
    253     m_panner = Panner::create(m_panningModel, sampleRate(), m_hrtfDatabaseLoader.get());
    254 
    255     AudioNode::initialize();
    256 }
    257 
    258 void PannerNode::uninitialize()
    259 {
    260     if (!isInitialized())
    261         return;
    262        
    263     m_panner = nullptr;
    264     AudioNode::uninitialize();
    265 }
    266 
    267259AudioListener& PannerNode::listener()
    268260{
     
    270262}
    271263
    272 void PannerNode::setPanningModel(PanningModelType model)
    273 {
    274     ASSERT(isMainThread());
    275 
    276     if (!m_panner.get() || model != m_panningModel) {
    277         // This synchronizes with process().
    278         Locker locker { m_processLock };
    279 
    280         m_panner = Panner::create(model, sampleRate(), m_hrtfDatabaseLoader.get());
     264void PannerNode::setPanningModelForBindings(PanningModelType model)
     265{
     266    ASSERT(isMainThread());
     267
     268    // This synchronizes with process().
     269    Locker locker { m_processLock };
     270    if (!m_panner || model != m_panningModel) {
     271        m_panner = Panner::create(model, sampleRate(), m_hrtfDatabaseLoader.ptr());
    281272        m_panningModel = model;
    282273    }
     
    337328}
    338329
    339 DistanceModelType PannerNode::distanceModel() const
    340 {
    341     return const_cast<PannerNode*>(this)->m_distanceEffect.model();
    342 }
    343 
    344 void PannerNode::setDistanceModel(DistanceModelType model)
     330DistanceModelType PannerNode::distanceModelForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS
     331{
     332    ASSERT(isMainThread());
     333    return m_distanceEffect.model();
     334}
     335
     336void PannerNode::setDistanceModelForBindings(DistanceModelType model)
    345337{
    346338    ASSERT(isMainThread());
     
    352344}
    353345
    354 ExceptionOr<void> PannerNode::setRefDistance(double refDistance)
     346ExceptionOr<void> PannerNode::setRefDistanceForBindings(double refDistance)
    355347{
    356348    ASSERT(isMainThread());
     
    366358}
    367359
    368 ExceptionOr<void> PannerNode::setMaxDistance(double maxDistance)
     360ExceptionOr<void> PannerNode::setMaxDistanceForBindings(double maxDistance)
    369361{
    370362    ASSERT(isMainThread());
     
    380372}
    381373
    382 ExceptionOr<void> PannerNode::setRolloffFactor(double rolloffFactor)
     374ExceptionOr<void> PannerNode::setRolloffFactorForBindings(double rolloffFactor)
    383375{
    384376    ASSERT(isMainThread());
     
    394386}
    395387
    396 ExceptionOr<void> PannerNode::setConeOuterGain(double gain)
     388ExceptionOr<void> PannerNode::setConeOuterGainForBindings(double gain)
    397389{
    398390    ASSERT(isMainThread());
     
    408400}
    409401
    410 void PannerNode::setConeOuterAngle(double angle)
     402void PannerNode::setConeOuterAngleForBindings(double angle)
    411403{
    412404    ASSERT(isMainThread());
     
    418410}
    419411
    420 void PannerNode::setConeInnerAngle(double angle)
     412void PannerNode::setConeInnerAngleForBindings(double angle)
    421413{
    422414    ASSERT(isMainThread());
     
    516508bool PannerNode::requiresTailProcessing() const
    517509{
     510    if (!m_processLock.tryLock())
     511        return true;
     512    Locker locker { AdoptLock, m_processLock };
    518513    // If there's no internal panner method set up yet, assume we require tail
    519514    // processing in case the HRTF panner is set later, which does require tail
     
    540535}
    541536
     537double PannerNode::tailTime() const
     538{
     539    if (!m_processLock.tryLock())
     540        return std::numeric_limits<double>::infinity();
     541    Locker locker { AdoptLock, m_processLock };
     542    return m_panner ? m_panner->tailTime() : 0;
     543}
     544
     545double PannerNode::latencyTime() const
     546{
     547    if (!m_processLock.tryLock())
     548        return std::numeric_limits<double>::infinity();
     549    Locker locker { AdoptLock, m_processLock };
     550    return m_panner ? m_panner->latencyTime() : 0;
     551}
     552
    542553} // namespace WebCore
    543554
  • trunk/Source/WebCore/Modules/webaudio/PannerNode.h

    r274650 r278284  
    6262    void process(size_t framesToProcess) override;
    6363    void processOnlyAudioParams(size_t framesToProcess) final;
    64     void initialize() override;
    65     void uninitialize() override;
    6664
    6765    // Listener
     
    6967
    7068    // Panning model
    71     PanningModelType panningModel() const { return m_panningModel; }
    72     void setPanningModel(PanningModelType);
     69    PanningModelType panningModelForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_panningModel; }
     70    void setPanningModelForBindings(PanningModelType);
    7371
    7472    // Position
    75     FloatPoint3D position() const;
    7673    ExceptionOr<void> setPosition(float x, float y, float z);
    77     AudioParam& positionX() { return m_positionX.get(); }
    78     AudioParam& positionY() { return m_positionY.get(); }
    79     AudioParam& positionZ() { return m_positionZ.get(); }
     74    AudioParam& positionX() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_positionX.get(); }
     75    AudioParam& positionY() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_positionY.get(); }
     76    AudioParam& positionZ() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_positionZ.get(); }
    8077
    8178    // Orientation
    82     FloatPoint3D orientation() const;
    8379    ExceptionOr<void> setOrientation(float x, float y, float z);
    84     AudioParam& orientationX() { return m_orientationX.get(); }
    85     AudioParam& orientationY() { return m_orientationY.get(); }
    86     AudioParam& orientationZ() { return m_orientationZ.get(); }
     80    AudioParam& orientationX() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_orientationX.get(); }
     81    AudioParam& orientationY() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_orientationY.get(); }
     82    AudioParam& orientationZ() WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_orientationZ.get(); }
    8783
    8884    // Distance parameters
    89     DistanceModelType distanceModel() const;
    90     void setDistanceModel(DistanceModelType);
     85    DistanceModelType distanceModelForBindings() const;
     86    void setDistanceModelForBindings(DistanceModelType);
    9187
    92     double refDistance() const { return m_distanceEffect.refDistance(); }
    93     ExceptionOr<void> setRefDistance(double);
     88    double refDistanceForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_distanceEffect.refDistance(); }
     89    ExceptionOr<void> setRefDistanceForBindings(double);
    9490
    95     double maxDistance() const { return m_distanceEffect.maxDistance(); }
    96     ExceptionOr<void> setMaxDistance(double);
     91    double maxDistanceForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_distanceEffect.maxDistance(); }
     92    ExceptionOr<void> setMaxDistanceForBindings(double);
    9793
    98     double rolloffFactor() const { return m_distanceEffect.rolloffFactor(); }
    99     ExceptionOr<void> setRolloffFactor(double);
     94    double rolloffFactorForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_distanceEffect.rolloffFactor(); }
     95    ExceptionOr<void> setRolloffFactorForBindings(double);
    10096
    10197    // Sound cones - angles in degrees
    102     double coneInnerAngle() const { return m_coneEffect.innerAngle(); }
    103     void setConeInnerAngle(double);
     98    double coneInnerAngleForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_coneEffect.innerAngle(); }
     99    void setConeInnerAngleForBindings(double);
    104100
    105     double coneOuterAngle() const { return m_coneEffect.outerAngle(); }
    106     void setConeOuterAngle(double);
     101    double coneOuterAngleForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_coneEffect.outerAngle(); }
     102    void setConeOuterAngleForBindings(double);
    107103
    108     double coneOuterGain() const { return m_coneEffect.outerGain(); }
    109     ExceptionOr<void> setConeOuterGain(double);
     104    double coneOuterGainForBindings() const WTF_IGNORES_THREAD_SAFETY_ANALYSIS { ASSERT(isMainThread()); return m_coneEffect.outerGain(); }
     105    ExceptionOr<void> setConeOuterGainForBindings(double);
    110106   
    111107    ExceptionOr<void> setChannelCount(unsigned) final;
    112108    ExceptionOr<void> setChannelCountMode(ChannelCountMode) final;
    113109
    114     void azimuthElevation(double* outAzimuth, double* outElevation);
    115 
    116     double tailTime() const override { return m_panner ? m_panner->tailTime() : 0; }
    117     double latencyTime() const override { return m_panner ? m_panner->latencyTime() : 0; }
     110    double tailTime() const final;
     111    double latencyTime() const final;
    118112
    119113private:
    120114    PannerNode(BaseAudioContext&, const PannerOptions&);
    121115
    122     void calculateAzimuthElevation(double* outAzimuth, double* outElevation, const FloatPoint3D& position, const FloatPoint3D& listenerPosition, const FloatPoint3D& listenerForward, const FloatPoint3D& listenerUp);
    123     float calculateDistanceConeGain(const FloatPoint3D& position, const FloatPoint3D& orientation, const FloatPoint3D& listenerPosition);
     116    void calculateAzimuthElevation(double* outAzimuth, double* outElevation, const FloatPoint3D& position, const FloatPoint3D& listenerPosition, const FloatPoint3D& listenerForward, const FloatPoint3D& listenerUp) WTF_REQUIRES_LOCK(m_processLock);
     117    float calculateDistanceConeGain(const FloatPoint3D& position, const FloatPoint3D& orientation, const FloatPoint3D& listenerPosition) WTF_REQUIRES_LOCK(m_processLock);
    124118
    125119    // Returns the combined distance and cone gain attenuation.
    126     float distanceConeGain();
     120    float distanceConeGain() WTF_REQUIRES_LOCK(m_processLock);
    127121
    128122    bool requiresTailProcessing() const final;
    129123
    130     void processSampleAccurateValues(AudioBus* destination, const AudioBus* source, size_t framesToProcess);
    131     bool hasSampleAccurateValues() const;
    132     bool shouldUseARate() const;
     124    void azimuthElevation(double* outAzimuth, double* outElevation) WTF_REQUIRES_LOCK(m_processLock);
     125    void processSampleAccurateValues(AudioBus* destination, const AudioBus* source, size_t framesToProcess) WTF_REQUIRES_LOCK(m_processLock);
     126    bool hasSampleAccurateValues() const WTF_REQUIRES_LOCK(m_processLock);
     127    bool shouldUseARate() const WTF_REQUIRES_LOCK(m_processLock);
    133128
    134     std::unique_ptr<Panner> m_panner;
    135     PanningModelType m_panningModel;
     129    FloatPoint3D position() const WTF_REQUIRES_LOCK(m_processLock);
     130    FloatPoint3D orientation() const WTF_REQUIRES_LOCK(m_processLock);
     131
     132    Ref<HRTFDatabaseLoader> m_hrtfDatabaseLoader;
     133    PanningModelType m_panningModel WTF_GUARDED_BY_LOCK(m_processLock);
     134    std::unique_ptr<Panner> m_panner WTF_GUARDED_BY_LOCK(m_processLock);
    136135
    137136    // Gain
    138     DistanceEffect m_distanceEffect;
    139     ConeEffect m_coneEffect;
     137    DistanceEffect m_distanceEffect WTF_GUARDED_BY_LOCK(m_processLock);
     138    ConeEffect m_coneEffect WTF_GUARDED_BY_LOCK(m_processLock);
    140139   
    141     Ref<AudioParam> m_positionX;
    142     Ref<AudioParam> m_positionY;
    143     Ref<AudioParam> m_positionZ;
     140    Ref<AudioParam> m_positionX WTF_GUARDED_BY_LOCK(m_processLock);
     141    Ref<AudioParam> m_positionY WTF_GUARDED_BY_LOCK(m_processLock);
     142    Ref<AudioParam> m_positionZ WTF_GUARDED_BY_LOCK(m_processLock);
    144143   
    145     Ref<AudioParam> m_orientationX;
    146     Ref<AudioParam> m_orientationY;
    147     Ref<AudioParam> m_orientationZ;
    148 
    149     // HRTF Database loader
    150     RefPtr<HRTFDatabaseLoader> m_hrtfDatabaseLoader;
     144    Ref<AudioParam> m_orientationX WTF_GUARDED_BY_LOCK(m_processLock);
     145    Ref<AudioParam> m_orientationY WTF_GUARDED_BY_LOCK(m_processLock);
     146    Ref<AudioParam> m_orientationZ WTF_GUARDED_BY_LOCK(m_processLock);
    151147
    152148    // Synchronize process() with setting of the panning model, source's location
  • trunk/Source/WebCore/Modules/webaudio/PannerNode.idl

    r276715 r278284  
    3232
    3333    // Default model for stereo is equalpower
    34     attribute PanningModelType panningModel;
     34    [ImplementedAs=panningModelForBindings] attribute PanningModelType panningModel;
    3535
    3636    // Uses a 3D cartesian coordinate system
     
    3939   
    4040    // Default distance model is inverse
    41     attribute DistanceModelType distanceModel;
     41    [ImplementedAs=distanceModelForBindings] attribute DistanceModelType distanceModel;
    4242
    43     attribute double refDistance;
    44     attribute double maxDistance;
    45     attribute double rolloffFactor;
     43    [ImplementedAs=refDistanceForBindings] attribute double refDistance;
     44    [ImplementedAs=maxDistanceForBindings] attribute double maxDistance;
     45    [ImplementedAs=rolloffFactorForBindings] attribute double rolloffFactor;
    4646
    4747    // Directional sound cone
    48     attribute double coneInnerAngle;
    49     attribute double coneOuterAngle;
    50     attribute double coneOuterGain;
     48    [ImplementedAs=coneInnerAngleForBindings] attribute double coneInnerAngle;
     49    [ImplementedAs=coneOuterAngleForBindings] attribute double coneOuterAngle;
     50    [ImplementedAs=coneOuterGainForBindings] attribute double coneOuterGain;
    5151   
    5252    // Position of audio source in 3D Cartesian system
  • trunk/Source/WebCore/platform/audio/Distance.h

    r267544 r278284  
    4949    double gain(double distance);
    5050
    51     DistanceModelType model() { return m_model; }
     51    DistanceModelType model() const { return m_model; }
    5252
    5353    void setModel(DistanceModelType model, bool clamped)
Note: See TracChangeset for help on using the changeset viewer.