{"id":351,"date":"2020-12-13T15:31:29","date_gmt":"2020-12-13T19:31:29","guid":{"rendered":"http:\/\/blog.labhackers.com\/?p=351"},"modified":"2020-12-21T12:28:45","modified_gmt":"2020-12-21T16:28:45","slug":"windows-10-display-timing","status":"publish","type":"post","link":"https:\/\/blog.labhackers.com\/?p=351","title":{"rendered":"Windows 10 Display Timing"},"content":{"rendered":"\n<p><sup>Originally posted on &nbsp;March 22, 2019.<\/sup><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"640\" height=\"480\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-1.png\" alt=\"\" class=\"wp-image-414\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-1.png 640w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-1-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><\/figure>\n\n\n\n<p>It looks like&nbsp;<em>you need to be careful with your display settings in Windows 10<\/em>, otherwise Windows 10 could be adding an extra frame delay to the display update times reported by your experiment software.<\/p>\n\n\n\n<p>We have found that if the Windows 10 Display -&gt; Scale (and layout) setting is not set to 100%, the Windows 10 operating system adds an extra frame delay to the actual monitor update time. When using a 70 Hz monitor for example, this adds an extra 14.3 msec delay, causing the reported display update time to be off by one retrace as well.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"632\" height=\"119\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro.png\" alt=\"\" class=\"wp-image-416\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro.png 632w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro-300x56.png 300w\" sizes=\"auto, (max-width: 632px) 100vw, 632px\" \/><figcaption>Windows 10 Monitor Scale Setting @ 100% results in expected display change timing.<\/figcaption><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"632\" height=\"119\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro2.png\" alt=\"\" class=\"wp-image-417\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro2.png 632w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/intro2-300x56.png 300w\" sizes=\"auto, (max-width: 632px) 100vw, 632px\" \/><figcaption><em>Windows 10 Monitor Scale Setting @ 125% results in extra 1 frame (monitor retrace) delay.<\/em><\/figcaption><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Procedure<\/h2>\n\n\n\n<p>Using the&nbsp;<a href=\"https:\/\/www.labhackers.com\/millikey.html\" target=\"_blank\" rel=\"noreferrer noopener\">MilliKey DeLux light sensor upgrade<\/a>&nbsp;and&nbsp;<a href=\"https:\/\/www.psychopy.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">PsychoPy3<\/a>&nbsp;we compared the display update time reported by&nbsp;<a href=\"https:\/\/psychopy.org\/api\/visual\/window.html#psychopy.visual.Window.flip\" target=\"_blank\" rel=\"noreferrer noopener\">psychopy.visual.Window.flip()<\/a>&nbsp;to the time the experiment received a 1000 Hz USB serial event triggered by the MilliKey DeLux light sensor in response to each screen brightness change.<\/p>\n\n\n\n<p>In this test we used was a Dell G5 15 laptop running Windows 10 with NVIDIA graphics connected to a SyncMaster P2770 LCD monitor. The stated response time of the SyncMaster P2770 is 1 msec, which probably means it is closer to 2 msec peak to peak.<\/p>\n\n\n\n<p>A Python 3 test script used the open source PsychoPy3 experiment package to make 1000 dark -&gt; light display changes, recording the flip() time of each dark-&gt;light change. The same script was connected to a MilliKey with the DeLux light sensor via USB Serial.<\/p>\n\n\n\n<p>The<a href=\"https:\/\/www.labhackers.com\/millikey.html\" target=\"_blank\" rel=\"noreferrer noopener\">&nbsp;MilliKey DeLux<\/a>&nbsp;was configured to generate a USB Serial event after the DeLux light sensor detected the monitor brightness crossed a digital threshold level. The appropriate light sensor threshold level for the monitor being tested was set by the test script itself.<\/p>\n\n\n\n<p>The&nbsp;<a href=\"https:\/\/www.labhackers.com\/millikey.html\" target=\"_blank\" rel=\"noreferrer noopener\">MilliKey DeLux<\/a>&nbsp;can be configured to send the USB Serial event a fixed number of msec after the actual light sensor trigger time, called the trigger offset. In this test we set the trigger offset to 7 msec. This allows the USB Serial event to be sent, and received, by the experiment program at a time when the program can rapidly receive and process the event.<\/p>\n\n\n\n<p>The same test script and hardware was used in both a 100% and 125% scaling condition. The time difference between receiving the DeLux USB serial trigger event and the time reported from the win.flip() for each dark-light transition was calculated, subtracting the 7 msec offset the DeLux was configured to use.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Results<\/h2>\n\n\n\n<p>USB serial events sent by the&nbsp;<a href=\"https:\/\/www.labhackers.com\/millikey.html\" target=\"_blank\" rel=\"noreferrer noopener\">MilliKey DeLux<\/a>&nbsp;will add an average of 0.5 msec to these display change delay results. This has not been corrected for in the following plots.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"640\" height=\"480\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-2.png\" alt=\"\" class=\"wp-image-418\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-2.png 640w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_100_vs_125-2-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><figcaption>Average Display Change Delay (Error) Bars and CI lines. Using 125% scaling causes an extra frame delay.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">100% Scaling<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"640\" height=\"480\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay.png\" alt=\"\" class=\"wp-image-419\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay.png 640w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><figcaption>Display change latency of SyncMaster P2770 LCD monitor @ 100% scaling setting is reasonable given LCD response time . Tested using MilliKey DeLux light sensor.<br><br>Display Update Delay Stats<br>Count: (1000,)<br>Average: 2.335 msec<br>Median: 2.231 msec<br>Min: 1.000 msec<br>Max: 4.556 msec<br>Stdev: 0.517 msec<\/figcaption><\/figure>\n\n\n\n<p>When 100% scaling is being used, the display update time reported by win.flip() looks accurate given the LCD monitor response time.<\/p>\n\n\n\n<p>Note that the bi-modal nature of the above delay distribution, with peaks at approximately 2 and 3 msec, is likely a result of quantization caused by the 1000 Hz MilliKey USB serial report rate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">125% Scaling<\/h3>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"640\" height=\"480\" src=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay_125x_display_setting.png\" alt=\"\" class=\"wp-image-420\" srcset=\"https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay_125x_display_setting.png 640w, https:\/\/blog.labhackers.com\/wp-content\/uploads\/2020\/12\/win10_p3_retrace_delay_125x_display_setting-300x225.png 300w\" sizes=\"auto, (max-width: 640px) 100vw, 640px\" \/><figcaption><em>Extra frame delay can be seen when Windows 10 Display Scaling setting is @ 125%. Tested using MilliKey DeLux light sensor.<\/em><\/figcaption><\/figure>\n\n\n\n<p>The results clearly show that the Windows 10 operating system is adding an extra frame delay when the Display Scaling setting is equal to 125%.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Code<\/h2>\n\n\n\n<p>The test script is based on one of the MilliKey DeLux Python examples that can be&nbsp;<a href=\"https:\/\/www.labhackers.com\/downloads.html\" target=\"_blank\" rel=\"noreferrer noopener\">downloaded from our website<\/a>. The test procedure logic is in the runDisplayTimingTest() function.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">#!\/usr\/bin\/env python\n# -*- coding: utf-8 -*-\nfrom __future__ import absolute_import, division, print_function\nfrom psychopy import visual, event, core\nimport json\nimport serial\nimport numpy\nimport time\n\n# Serial Port of MilliKey.\n#mK_seria1_port = '\/dev\/cu.usbmodem4998701'\nmK_seria1_port = 'COM112'\n\n# Number of dark -&gt; light display change tests to run\ntest_count = 1000\n\n# Number of light frames to display after dark-&gt;light transition\nlight_frame_count = 10\n\n# Number seconds to wait between each test iteration.\niti=0.4\n\n# Number of msec MilliKey waits before sending serial trigger after \n# light threshold is crossed. \ntrig_delay=7\n\n# Maximum number of trigger events MilliKey will send on each test iteration. \n# Useful for limiting the trigger repeating every retrace on CRT monitors for\n# example.\nmax_trig_count =  2\n\n# True == Run simple auto threshold leveling routine before running the test.\n# False == Use threshold level stored in MilliKey device.\nautothresh = True\n\n# Multiplier for calculating threshold level from dark screen max reading\n# For LCD, suggest 1.5 - 2.5\n# For CRT, suggest 5.0 - 10.0\nthresh_scaling_factor = 2\n\nTRIG_RISING_SERIAL = 1\n\ndef runDisplayTimingTest():\n    results = numpy.zeros(test_count, dtype=numpy.float64)\n    mk_sconn = init_millikey_delux()\n    win, (white_stim, dark_stim, txt_stim1, txt_stim2) = initPsychoPyGraphics()\n\n    start_key = event.waitKeys()\n\n    if autothresh:\n        lowlight_stats = getLightSensorStats(mk_sconn, win, dark_stim)\n        ls_threshold = lowlight_stats.get('max')*thresh_scaling_factor\n        print(\"Using threshold level %d for test.\\n\"%(ls_threshold))\n\n    print(\"test_num\\ttrig_rx_time\\tflip_time\\ttrig_delay\\tdisplay_latency\\ttrig_msg\")\n\n    # run test_count loops\n    count=0\n    while count &lt; test_count:\n        # Draw black screen and flip to it.\n        dark_stim.draw()\n        win.flip()\n        # Wait ~iti seconds with display black, starting light sensor\n        # 1\/2 way through.\n        time.sleep(iti\/2)        \n        mk_sconn.write(b\"START_AIN %d\\n\"%(ls_threshold))\n        core.wait(iti\/2)\n        flush_serial_rx(mk_sconn)\n        # Draw white (target) stim and flip to it,\n        # getting time flip occurred (stim onset)\n        # time from psychopy.\n        white_stim.draw()\n        ftime = win.flip()\n\n        reading = mk_sconn.readline()\n        if reading:\n                t, d = core.getTime(), json.loads(reading[:-2])\n                tdelay = d['delay']\/1000.0\n                results[count]= t-ftime-tdelay\n                if d['count'] &gt; 1:\n                    print(\"Warning: Trigger number %d received; expecting 1\"%(d['count']))\n                print(\"%d\\t%.4f\\t%.4f\\t%.4f\\t%.4f\\t%s\"%(count+1, t, ftime,\n                                                    tdelay, results[count],str(d)))\n        else:\n            print(\"Error: MilliKey Serial Timeout.\")\n            win.close()\n            mk_sconn.close()\n            raise RuntimeError(\"Error: MilliKey Serial Timeout.\")\n\n\n        # Display white screen for n frames.\n        for lf in range(light_frame_count):\n            white_stim.draw()\n            win.flip()\n\n        mk_sconn.write(b\"STOP_AIN\\n\")\n        count+=1\n\n    win.close()\n    mk_sconn.close()\n    return results\n\ndef sendSerial(mk_sconn, txdata, wait=0.050, is_json=True):\n    rx = None\n    mk_sconn.write(\"{}\\n\".format(txdata).encode('utf-8'))\n    stime = core.getTime()\n    rx = mk_sconn.readline()\n    while len(rx) == 0 and core.getTime()-stime &lt; wait:\n        rx = mk_sconn.readline()\n    if rx and is_json:\n        try:\n            return json.loads(rx[:-2])\n        except:\n            return rx[:-2]\n    if rx:\n        return rx[:-2]\n\ndef flush_serial_rx(mk_sconn):\n    while mk_sconn.readline():\n        pass\n    \ndef getLightSensorStats(mk_sconn, win, stim):\n    for i in range(4):\n        stim.draw()\n        win.flip()\n    mk_sconn.write(b\"START_AIN\\n\")\n    for i in range(10):\n        stim.draw()\n        win.flip()\n    mk_sconn.write(b\"STOP_AIN\\n\")\n    mk_sconn.write(b\"GET AIN_STATS\\n\")\n    stats = mk_sconn.readline()\n    return json.loads(stats)\n\ndef init_millikey_delux():\n    mk_sconn = serial.Serial(mK_seria1_port, baudrate=128000, timeout=0.05)\n    print(sendSerial(mk_sconn, 'SET AIN_TRIG_DELAY %d'%(trig_delay),\n                     is_json=False))\n    print(sendSerial(mk_sconn, 'SET AIN_TRIG_MODE %d %d'%(TRIG_RISING_SERIAL,\n                                                          max_trig_count),\n                                                          is_json=False))\n\n    ain_stats= sendSerial(mk_sconn, \"GET AIN_STATS\")\n    ain_res = ain_stats.get('res')\n    if ain_res &gt; 100: # ain_res &lt; 100 means light sensor is connected\n        print(\"Error Light Sensor is not connected to Analog Input Jack.\")\n        mk_sconn.close()\n        raise RuntimeError(\"Analog Input Error. Check connection.\")\n    return mk_sconn\n\ndef initPsychoPyGraphics():\n    # Create a full screen window and two full screen stim.\n    win = visual.Window(fullscr=True, screen=0)\n    white_stim = visual.ShapeStim(win, lineColor='white', fillColor='white',\n                                  vertices=((-1,-1),(1,-1),(1,1),(-1,1)))\n    dark_stim = visual.ShapeStim(win, lineColor='black', fillColor='black',\n                                 vertices=((-1,-1),(1,-1),(1,1),(-1,1)))\n    txt_stim1 = visual.TextStim(win,\n                                \"Place DeLux Light Sensor in Top Left Corner.\",\n                                color=(0.0,1.0,0.0), height=0.05)\n    txt_stim2 = visual.TextStim(win, \"Press any Key to Start Test.\",\n                                pos=(0.0,-0.1), color=(0.0,1.0,0.0),\n                                height=0.05)\n    white_stim.draw()\n    dark_stim.draw()\n    txt_stim1.draw()\n    txt_stim2.draw()\n    win.flip()\n    return win, (white_stim, dark_stim, txt_stim1, txt_stim2)\n\n# Plot Results\ndef createHistogram(data, title=\"Display\\ Update\\ Delay\", \n                    xlabel=\"Delay (milliseconds)\",\n                    ylabel=\"Probability density\", nbins = 50):\n    try:\n        import matplotlib.pyplot as plt\n        mu, sigma = data.mean(), data.std()\n        # the histogram of the data\n        n, bins, patches = plt.hist(data, nbins, density=1,\n                                    facecolor='green', alpha=0.75)\n        # add a 'best fit' line\n        y = ((1 \/ (numpy.sqrt(2 * numpy.pi) * sigma)) *\n             numpy.exp(-0.5 * (1 \/ sigma * (bins - mu))**2))\n        plt.plot(bins, y, '--')\n        plt.xlabel(xlabel)\n        plt.ylabel(ylabel)\n        if title is None:\n            title = r\"Histogram\"\n        title = r'$\\mathrm{%s:}\\ \\mu=%.3f,\\ \\sigma=%.3f$'%(title,mu,sigma)\n        plt.title(title)\n        plt.grid(True)\n        plt.tight_layout()\n        plt.show()\n    except:\n        print(\"Could not create Histogram:\\n\\ttitle: {}\\n\\tlabel: {}\\n\\tylabel: {}\".format(title, xlabel, ylabel))\n    \nif __name__ == '__main__':\n results = runDisplayTimingTest()\n \n    # Convert times to msec.\n    results = results * 1000.0\n    \n    print('\\n\\n')\n    print(\"Display Update Delay Stats\")\n    print(\"\\tCount: {}\".format(results.shape))\n    print(\"\\tAverage: {:.3f} msec\".format(results.mean()))\n    print(\"\\tMedian: {:.3f} msec\".format(numpy.median(results)))\n    print(\"\\tMin: {:.3f} msec\".format(results.min()))\n    print(\"\\tMax: {:.3f} msec\".format(results.max()))\n    print(\"\\tStdev: {:.3f} msec\".format(results.std()))\n    \n    createHistogram(results)<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusions<\/h2>\n\n\n\n<p>Next time you go to collect data on a Windows 10 machine, we would suggest you first make sure to check the Windows 10 Display Scaling setting and ensure it is at 100%.<\/p>\n\n\n\n<p>It is probably worth pointing out that the extra delay seen in the 125% scaling condition does&nbsp;<strong>not&nbsp;<\/strong>effect stimulus duration: it adds one retrace interval to the start&nbsp;<em>and&nbsp;<\/em>end time of a stimulus.<\/p>\n\n\n\n<p>There are likely other conditions that could cause the display timing of your experiment computer to be different than what you think or assume it to be. Therefore, the best option in our opinion is to&nbsp;<em>test your own experiment setup on a regular basis<\/em>. This is not as hard to do, time consuming, or expensive as you might think. Please checkout the<a href=\"https:\/\/www.labhackers.com\/millikey.html\" target=\"_blank\" rel=\"noreferrer noopener\">&nbsp;MilliKey DeLux<\/a>&nbsp;and consider using it the next time you need to do some timing validation of your experiment setup. Testing display change timing has never been easier and more affordable!<\/p>\n\n\n\n<p>Happy Hacking!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Originally posted on &nbsp;March 22, 2019. It looks like&nbsp;you need to be careful with your display settings in Windows 10, otherwise Windows 10 could be adding an extra frame delay to the display update times reported by your experiment software. We have found that if the Windows 10 Display -&gt; Scale (and layout) setting is&hellip; <a class=\"more-link\" href=\"https:\/\/blog.labhackers.com\/?p=351\">Continue reading <span class=\"screen-reader-text\">Windows 10 Display Timing<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"spay_email":"","footnotes":""},"categories":[19,20],"tags":[21,25,18,10,14,26],"class_list":["post-351","post","type-post","status-publish","format-standard","hentry","category-display-timing","category-millikey-delux-light-sensor","tag-display","tag-millikey-delux","tag-psychopy","tag-python","tag-timing","tag-windows-10","entry"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/posts\/351","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=351"}],"version-history":[{"count":8,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/posts\/351\/revisions"}],"predecessor-version":[{"id":450,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=\/wp\/v2\/posts\/351\/revisions\/450"}],"wp:attachment":[{"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=351"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=351"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.labhackers.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=351"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}