Merge lp:~widelands-dev/widelands/graphic_resetting into lp:widelands

Proposed by SirVer
Status: Merged
Merged at revision: 6635
Proposed branch: lp:~widelands-dev/widelands/graphic_resetting
Merge into: lp:widelands
Diff against target: 586 lines (+148/-98)
14 files modified
src/graphic/font_handler.cc (+4/-0)
src/graphic/font_handler.h (+4/-0)
src/graphic/graphic.cc (+32/-22)
src/graphic/graphic.h (+12/-6)
src/graphic/image_cache.h (+1/-0)
src/graphic/render/sdl_surface.cc (+2/-1)
src/graphic/render/sdl_surface.h (+6/-2)
src/graphic/surface_cache.cc (+8/-0)
src/graphic/surface_cache.h (+3/-0)
src/helper.cc (+19/-1)
src/helper.h (+3/-0)
src/ui_basic/panel.cc (+33/-9)
src/wlapplication.cc (+21/-42)
src/wlapplication.h (+0/-15)
To merge this branch: bzr merge lp:~widelands-dev/widelands/graphic_resetting
Reviewer Review Type Date Requested Status
cghislai (community) Approve
Tino Approve
Review via email: mp+159985@code.launchpad.net

Description of the change

There is at least one critical bug lurking in this code - but I wasn't able to produce a reliable crash scenario, not did I see the bug. Reaching out for a bunch more eyes :)

To post a comment you must log in.
Revision history for this message
cghislai (charlyghislain) wrote :

I get an invalid pointer when trying to free colormap (Colormap::~Colormap at line 66)
This happens when i change to opengl rendering after having played with non-opengl graphics

Additionally, i get a crash at GLSurfaceTexture::setup_gl(), line 280. This happens when using opengl and buildings statistics. The cause seems to be the attempt to render an empty text. At SubTagRenderNode::render(SurfaceCache* surface_cache), the following code throws the exception in place of the crash:

virtual Surface* render(SurfaceCache* surface_cache) {
  if (width() == 0 && height() == 0)
   throw RT::Exception("RT render: Tried to render empty surface");

Handling special cases to prevent blitting to happen for empty text (but with rich text tags), this latter crash can be avoided.

Using trunk, i get the crash at fonthandler_1.cc:99 described in the bug report when trying to render the loading screen tip using opengl.

Revision history for this message
Tino (tino79) wrote :

Ok, got it to compile with mingw, but it does not work on windows:
Widelands starts fine but as soon as i click to proceed to the main menu Widelands just closes.
No output into stderr.txt and stdout.txt just ends after the graphics initialization part

review: Needs Fixing
Revision history for this message
SirVer (sirver) wrote :

Thanks for the testing - I'll review the branch again and try to figure out what is wrong. That it crashes this early for you is an indication that something very basic is wrong. Maybe valgrind can help me out.

Revision history for this message
Tino (tino79) wrote :

It seems like a SIGTRAP error as described here: http://stackoverflow.com/questions/1621059/breakpoints-out-of-nowhere-when-debugging-with-gdb-inside-ntdll

So with "handle SIGTRAP nostop" widelands continues to the main menu.

I can't find the code point in widelands where this happens, will try windbg...

Revision history for this message
Tino (tino79) wrote :

Ok, the cuplrit seems to be the truncating in helper.cc::random_string()

    buffer[nlen] = '\0';

Afterwards the SIGTRAP occurs at some point when boost tries to delete the buffer.
I've committed a small change which fixes the code at least for my system.
Perhaps using boost:UUID could replace the manual string generation?

Now this trunk allows me to switch the graphic mode ingame, but only before i've run a game. If I go back from a game to the main menu and switch the graphics mode widelands still quits.

Revision history for this message
cghislai (charlyghislain) wrote :

at r6566, I can play normally.

I get errors when graphics are reset after a game:
segfault at free(colormap); in Colormap destructor
invalid pointer at delete texutre in Graphic::cleanup()

Revision history for this message
cghislai (charlyghislain) wrote :

This last commit fixes the crash on my side

Revision history for this message
cghislai (charlyghislain) wrote :

Also it seems graphics are restted far too often. Once on game start, then once each time mainmenu is displayed, then once on game loading, at least.

Revision history for this message
SirVer (sirver) wrote :

Both of the bugs you guys were fixing are really embarrassing - but thanks for that :).

I will check and see if the branch is now crash free for me as well and then really propose for merging. This will likely not happen till the weekend though.

Revision history for this message
Tino (tino79) wrote :

Ok, i can't trigger any more crashes now, seems to work perfectly.

review: Approve
Revision history for this message
Jens Beyer (qcumber-some) wrote :

I also can't make it crash anymore :-)

Revision history for this message
cghislai (charlyghislain) wrote :

I also approve this to be merged as I need this code to be able to spot issues related to opengl rendering.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/graphic/font_handler.cc'
2--- src/graphic/font_handler.cc 2013-06-15 13:38:19 +0000
3+++ src/graphic/font_handler.cc 2013-07-16 20:53:40 +0000
4@@ -111,9 +111,13 @@
5
6
7 Font_Handler::~Font_Handler() {
8+ flush();
9 Font::shutdown();
10 }
11
12+void Font_Handler::flush() {
13+ d.reset(new Data);
14+}
15
16 /*
17 * Returns the height of the font, in pixels.
18
19=== modified file 'src/graphic/font_handler.h'
20--- src/graphic/font_handler.h 2013-06-15 13:38:19 +0000
21+++ src/graphic/font_handler.h 2013-07-16 20:53:40 +0000
22@@ -20,6 +20,7 @@
23 #ifndef FONT_HANDLER_H
24 #define FONT_HANDLER_H
25
26+#include <string>
27 #include <boost/scoped_ptr.hpp>
28
29 #include "point.h"
30@@ -59,6 +60,9 @@
31 uint32_t wrap = std::numeric_limits<uint32_t>::max());
32 uint32_t get_fontheight(const std::string & name, int32_t size);
33
34+ // Delete the whole cache.
35+ void flush();
36+
37 private:
38 struct Data;
39 boost::scoped_ptr<Data> d;
40
41=== modified file 'src/graphic/graphic.cc'
42--- src/graphic/graphic.cc 2013-07-14 17:41:02 +0000
43+++ src/graphic/graphic.cc 2013-07-16 20:53:40 +0000
44@@ -44,6 +44,7 @@
45
46 #include "animation.h"
47 #include "animation_gfx.h"
48+#include "font_handler.h"
49 #include "image.h"
50 #include "image_loader_impl.h"
51 #include "image_transformations.h"
52@@ -62,14 +63,9 @@
53 /**
54 * Initialize the SDL video mode.
55 */
56-Graphic::Graphic
57- (int32_t w, int32_t h,
58- int32_t bpp,
59- bool fullscreen,
60- bool opengl)
61+Graphic::Graphic()
62 :
63 m_fallback_settings_in_effect (false),
64- m_rendertarget (0),
65 m_nr_update_rects (0),
66 m_update_fullscreen(true),
67 image_loader_(new ImageLoaderImpl()),
68@@ -88,9 +84,12 @@
69 SDL_Surface * s = IMG_Load_RW(SDL_RWFromMem(fr.Data(0), fr.GetSize()), 1);
70 SDL_WM_SetIcon(s, 0);
71 SDL_FreeSurface(s);
72+}
73+
74+void Graphic::initialize(int32_t w, int32_t h, int32_t bpp, bool fullscreen, bool opengl) {
75+ cleanup();
76
77 // Set video mode using SDL. First collect the flags
78-
79 int32_t flags = 0;
80 g_opengl = false;
81 SDL_Surface * sdlsurface = 0;
82@@ -306,7 +305,6 @@
83 glEnable(GL_TEXTURE_2D);
84
85 GLSurfaceTexture::Initialize(use_arb);
86-
87 }
88
89 if (g_opengl)
90@@ -315,11 +313,11 @@
91 }
92 else
93 {
94- screen_.reset(new SDLSurface(sdlsurface));
95+ screen_.reset(new SDLSurface(sdlsurface, false));
96 }
97
98 m_sdl_screen = sdlsurface;
99- m_rendertarget = new RenderTarget(screen_.get());
100+ m_rendertarget.reset(new RenderTarget(screen_.get()));
101 }
102
103 bool Graphic::check_fallback_settings_in_effect()
104@@ -327,25 +325,27 @@
105 return m_fallback_settings_in_effect;
106 }
107
108-/**
109- * Free the surface
110-*/
111-Graphic::~Graphic()
112-{
113- BOOST_FOREACH(Texture* texture, m_maptextures)
114- delete texture;
115- delete m_rendertarget;
116-
117+void Graphic::cleanup() {
118+ flush_maptextures();
119 flush_animations();
120+ surface_cache_->flush();
121+ // TODO: this should really not be needed, but currently is :(
122+ if (UI::g_fh)
123+ UI::g_fh->flush();
124
125 if (g_opengl)
126 GLSurfaceTexture::Cleanup();
127 }
128
129+Graphic::~Graphic()
130+{
131+ cleanup();
132+}
133+
134 /**
135 * Return the screen x resolution
136 */
137-int32_t Graphic::get_xres() const
138+int32_t Graphic::get_xres()
139 {
140 return screen_->width();
141 }
142@@ -353,11 +353,21 @@
143 /**
144 * Return the screen x resolution
145 */
146-int32_t Graphic::get_yres() const
147+int32_t Graphic::get_yres()
148 {
149 return screen_->height();
150 }
151
152+int32_t Graphic::get_bpp()
153+{
154+ return m_sdl_screen->format->BitsPerPixel;
155+}
156+
157+bool Graphic::is_fullscreen()
158+{
159+ return m_sdl_screen->flags & SDL_FULLSCREEN;
160+}
161+
162 /**
163 * Return a pointer to the RenderTarget representing the screen
164 */
165@@ -365,7 +375,7 @@
166 {
167 m_rendertarget->reset();
168
169- return m_rendertarget;
170+ return m_rendertarget.get();
171 }
172
173 /**
174
175=== modified file 'src/graphic/graphic.h'
176--- src/graphic/graphic.h 2013-03-09 08:07:22 +0000
177+++ src/graphic/graphic.h 2013-07-16 20:53:40 +0000
178@@ -86,13 +86,18 @@
179 */
180 class Graphic {
181 public:
182- Graphic
183- (int32_t w, int32_t h, int32_t bpp,
184- bool fullscreen, bool opengl);
185+ Graphic();
186 ~Graphic();
187
188- int32_t get_xres() const;
189- int32_t get_yres() const;
190+ // Initialize or reinitialize the graphics system. Throws on error.
191+ void initialize
192+ (int32_t w, int32_t h, int32_t bpp, bool fullscreen, bool opengl);
193+
194+ int32_t get_xres();
195+ int32_t get_yres();
196+ int32_t get_bpp();
197+ bool is_fullscreen();
198+
199 RenderTarget * get_render_target();
200 void toggle_fullscreen();
201 void update_fullscreen();
202@@ -133,6 +138,7 @@
203 bool check_fallback_settings_in_effect();
204
205 private:
206+ void cleanup();
207 void save_png_(Surface & surf, StreamWrite*) const;
208
209 bool m_fallback_settings_in_effect;
210@@ -153,7 +159,7 @@
211 /// manipulation the screen context.
212 SDL_Surface * m_sdl_screen;
213 /// A RenderTarget for screen_. This is initialized during init()
214- RenderTarget * m_rendertarget;
215+ boost::scoped_ptr<RenderTarget> m_rendertarget;
216 /// keeps track which screen regions needs to be redrawn during the next
217 /// update(). Only used for SDL rendering.
218 SDL_Rect m_update_rects[MAX_RECTS];
219
220=== modified file 'src/graphic/image_cache.h'
221--- src/graphic/image_cache.h 2013-02-10 16:08:37 +0000
222+++ src/graphic/image_cache.h 2013-07-16 20:53:40 +0000
223@@ -25,6 +25,7 @@
224 #include <boost/utility.hpp>
225
226 #include "image.h"
227+
228 class IImageLoader;
229 class SurfaceCache;
230
231
232=== modified file 'src/graphic/render/sdl_surface.cc'
233--- src/graphic/render/sdl_surface.cc 2013-07-12 15:11:32 +0000
234+++ src/graphic/render/sdl_surface.cc 2013-07-16 20:53:40 +0000
235@@ -26,7 +26,8 @@
236 SDLSurface::~SDLSurface() {
237 assert(m_surface);
238
239- SDL_FreeSurface(m_surface);
240+ if (m_free_surface_on_delete)
241+ SDL_FreeSurface(m_surface);
242 }
243
244 const SDL_PixelFormat & SDLSurface::format() const {
245
246=== modified file 'src/graphic/render/sdl_surface.h'
247--- src/graphic/render/sdl_surface.h 2013-02-10 16:41:12 +0000
248+++ src/graphic/render/sdl_surface.h 2013-07-16 20:53:40 +0000
249@@ -33,10 +33,13 @@
250 */
251 class SDLSurface : public Surface {
252 public:
253- SDLSurface(SDL_Surface* surface) :
254+ // The surface set by SetVideoMode must not be freed according to the SDL
255+ // docs, so we need 'free_surface_on_delete'.
256+ SDLSurface(SDL_Surface* surface, bool free_surface_on_delete = true) :
257 m_surface(surface),
258 m_offsx(0), m_offsy(0),
259- m_w(surface->w), m_h(surface->h)
260+ m_w(surface->w), m_h(surface->h),
261+ m_free_surface_on_delete(free_surface_on_delete)
262 {}
263 virtual ~SDLSurface();
264
265@@ -72,6 +75,7 @@
266 int32_t m_offsx;
267 int32_t m_offsy;
268 uint16_t m_w, m_h;
269+ bool m_free_surface_on_delete;
270 };
271
272
273
274=== modified file 'src/graphic/surface_cache.cc'
275--- src/graphic/surface_cache.cc 2013-07-13 14:32:49 +0000
276+++ src/graphic/surface_cache.cc 2013-07-16 20:53:40 +0000
277@@ -39,6 +39,7 @@
278 virtual ~SurfaceCacheImpl();
279
280 // Implements SurfaceCache.
281+ virtual void flush();
282 virtual Surface* get(const string& hash);
283 virtual Surface* insert(const string& hash, Surface*, bool);
284
285@@ -64,9 +65,16 @@
286 };
287
288 SurfaceCacheImpl::~SurfaceCacheImpl() {
289+ flush();
290+}
291+
292+void SurfaceCacheImpl::flush() {
293 for (Container::iterator it = entries_.begin(); it != entries_.end(); ++it) {
294 delete it->second;
295 }
296+ entries_.clear();
297+ access_history_.clear();
298+ used_transient_memory_ = 0;
299 }
300
301 Surface* SurfaceCacheImpl::get(const string& hash) {
302
303=== modified file 'src/graphic/surface_cache.h'
304--- src/graphic/surface_cache.h 2013-07-13 14:32:49 +0000
305+++ src/graphic/surface_cache.h 2013-07-16 20:53:40 +0000
306@@ -38,6 +38,9 @@
307 SurfaceCache() {};
308 virtual ~SurfaceCache() {};
309
310+ /// Deletes all surfaces in the cache leaving it as if it were just created.
311+ virtual void flush() = 0;
312+
313 /// Returns an entry if it is cached, NULL otherwise.
314 virtual Surface* get(const std::string& hash) = 0;
315
316
317=== modified file 'src/helper.cc'
318--- src/helper.cc 2013-02-10 19:36:24 +0000
319+++ src/helper.cc 2013-07-16 20:53:40 +0000
320@@ -17,9 +17,16 @@
321 *
322 */
323
324+#include <cstdarg>
325+#include <string>
326+
327+#include <boost/random.hpp>
328+#include <boost/scoped_array.hpp>
329+
330 #include "helper.h"
331
332-#include <cstdarg>
333+using namespace std;
334+
335
336 /// Split a string by separators.
337 /// \note This ignores empty elements, so do not use this for example to split
338@@ -71,3 +78,14 @@
339 ((k.sym >= SDLK_WORLD_0) && (k.sym <= SDLK_WORLD_95)) ||
340 ((k.sym >= SDLK_KP0) && (k.sym <= SDLK_KP_EQUALS));
341 }
342+
343+static boost::random::mt19937 random_generator;
344+string random_string(const string& chars, int nlen) {
345+ boost::random::uniform_int_distribution<> index_dist(0, chars.size() - 1);
346+ boost::scoped_array<char> buffer(new char[nlen - 1]);
347+ for (int i = 0; i < nlen; ++i) {
348+ buffer[i] = chars[index_dist(random_generator)];
349+ }
350+ return string(buffer.get(),nlen);
351+}
352+
353
354=== modified file 'src/helper.h'
355--- src/helper.h 2013-02-10 19:36:24 +0000
356+++ src/helper.h 2013-07-16 20:53:40 +0000
357@@ -158,4 +158,7 @@
358
359 bool is_printable(SDL_keysym k);
360
361+/// Generate a random string of given size out of the given alphabet.
362+std::string random_string(const std::string& chars, int nlen);
363+
364 #endif
365
366=== modified file 'src/ui_basic/panel.cc'
367--- src/ui_basic/panel.cc 2013-03-02 20:35:18 +0000
368+++ src/ui_basic/panel.cc 2013-07-16 20:53:40 +0000
369@@ -17,15 +17,14 @@
370 *
371 */
372
373-#include <boost/concept_check.hpp>
374-
375 #include "constants.h"
376 #include "graphic/font_handler.h"
377 #include "graphic/font_handler1.h"
378 #include "graphic/graphic.h"
379-#include "graphic/in_memory_image.h"
380 #include "graphic/rendertarget.h"
381 #include "graphic/surface.h"
382+#include "graphic/surface_cache.h"
383+#include "helper.h"
384 #include "log.h"
385 #include "profile/profile.h"
386 #include "sound/sound_handler.h"
387@@ -34,8 +33,37 @@
388
389 #include "panel.h"
390
391+using namespace std;
392+
393 namespace UI {
394
395+namespace {
396+class CacheImage : public Image {
397+public:
398+ CacheImage(uint16_t w, uint16_t h) :
399+ width_(w), height_(h),
400+ hash_("cache_image_" + random_string("0123456789ABCDEFGH", 32)) {}
401+ virtual ~CacheImage() {}
402+
403+ // Implements Image.
404+ virtual uint16_t width() const {return width_;}
405+ virtual uint16_t height() const {return height_;}
406+ virtual const string& hash() const {return hash_;}
407+ virtual Surface* surface() const {
408+ Surface* rv = g_gr->surfaces().get(hash_);
409+ if (rv)
410+ return rv;
411+
412+ rv = g_gr->surfaces().insert(hash_, Surface::create(width_, height_), true);
413+ return rv;
414+ }
415+
416+private:
417+ const int16_t width_, height_;
418+ const string hash_;
419+};
420+
421+} // namespace
422 Panel * Panel::_modal = 0;
423 Panel * Panel::_g_mousegrab = 0;
424 Panel * Panel::_g_mousein = 0;
425@@ -835,12 +863,8 @@
426 uint32_t innerw = _w - (_lborder + _rborder);
427 uint32_t innerh = _h - (_tborder + _bborder);
428
429- if
430- (!_cache ||
431- _cache.get()->width() != innerw ||
432- _cache.get()->height() != innerh)
433- {
434- _cache.reset(new_in_memory_image("dummy_hash", Surface::create(innerw, innerh)));
435+ if (!_cache || _cache->width() != innerw || _cache->height() != innerh) {
436+ _cache.reset(new CacheImage(innerw, innerh));
437 _needdraw = true;
438 }
439
440
441=== modified file 'src/wlapplication.cc'
442--- src/wlapplication.cc 2013-07-15 05:18:12 +0000
443+++ src/wlapplication.cc 2013-07-16 20:53:40 +0000
444@@ -263,9 +263,6 @@
445 m_mouse_locked (0),
446 m_mouse_compensate_warp(0, 0),
447 m_should_die (false),
448-m_gfx_w(0), m_gfx_h(0), m_gfx_bpp(0),
449-m_gfx_fullscreen (false),
450-m_gfx_opengl (true),
451 m_default_datadirs (true),
452 #ifdef WIN32
453 m_homedir(FileSystem::GetHomedir() + "\\.widelands"),
454@@ -470,7 +467,6 @@
455 throw;
456 }
457 } else {
458-
459 g_sound_handler.start_music("intro");
460
461 {
462@@ -810,41 +806,31 @@
463 }
464
465 /**
466- * Initialize the graphics subsystem (or shutdown, if system == GFXSYS_NONE)
467+ * Initialize the graphics subsystem (or shutdown, if w and h are 0)
468 * with the given resolution.
469 * Throws an exception on failure.
470- *
471- * \note Because of the way pictures are handled now, this function must not be
472- * called while UI elements are active.
473- *
474- * \todo Ensure that calling this with active UI elements does barf
475- * \todo Document parameters
476 */
477-
478 void WLApplication::init_graphics
479 (const int32_t w, const int32_t h, const int32_t bpp,
480 const bool fullscreen, const bool opengl)
481 {
482- if
483- (w == m_gfx_w && h == m_gfx_h && bpp == m_gfx_bpp &&
484- fullscreen == m_gfx_fullscreen &&
485- opengl == m_gfx_opengl)
486+ if (!w && !h) { // shutdown.
487+ delete g_gr;
488+ g_gr = 0;
489 return;
490-
491- delete g_gr;
492- g_gr = 0;
493-
494- m_gfx_w = w;
495- m_gfx_h = h;
496- m_gfx_bpp = bpp;
497- m_gfx_fullscreen = fullscreen;
498- m_gfx_opengl = opengl;
499-
500-
501- // If we are not to be shut down
502- if (w && h) {
503- g_gr = new Graphic
504- (w, h, bpp, fullscreen, opengl);
505+ }
506+ assert(w > 0 && h > 0);
507+
508+ if (!g_gr) {
509+ g_gr = new Graphic();
510+ g_gr->initialize(w, h, bpp, fullscreen, opengl);
511+ } else {
512+ if
513+ (g_gr->get_xres() != w || g_gr->get_yres() != h || g_gr->get_bpp() != bpp
514+ || g_gr->is_fullscreen() != fullscreen || g_opengl != opengl)
515+ {
516+ g_gr->initialize(w, h, bpp, fullscreen, opengl);
517+ }
518 }
519 }
520
521@@ -856,7 +842,7 @@
522 init_graphics
523 (s.get_int("xres", XRES),
524 s.get_int("yres", YRES),
525- s.get_int("depth", 16),
526+ s.get_int("depth", 32),
527 s.get_bool("fullscreen", false),
528 s.get_bool("opengl", true));
529 }
530@@ -881,14 +867,12 @@
531 set_input_grab(s.get_bool("inputgrab", false));
532 set_mouse_swap(s.get_bool("swapmouse", false));
533
534- m_gfx_fullscreen = s.get_bool("fullscreen", false);
535-
536- m_gfx_opengl = s.get_bool("opengl", true);
537-
538 // KLUDGE!
539 // Without this the following config options get dropped by check_used().
540 // Profile needs support for a Syntax definition to solve this in a
541 // sensible way
542+ s.get_bool("fullscreen");
543+ s.get_bool("opengl");
544 s.get_int("xres");
545 s.get_int("yres");
546 s.get_int("border_snap_distance");
547@@ -1085,12 +1069,7 @@
548 SDL_EnableUNICODE(1); //needed by helper.h:is_printable()
549 SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL);
550
551- uint32_t xres = s.get_int("xres", XRES);
552- uint32_t yres = s.get_int("yres", YRES);
553-
554- init_graphics
555- (xres, yres, s.get_int("depth", 16),
556- m_gfx_fullscreen, m_gfx_opengl);
557+ refresh_graphics();
558
559 // Start the audio subsystem
560 // must know the locale before calling this!
561
562=== modified file 'src/wlapplication.h'
563--- src/wlapplication.h 2013-05-20 19:40:13 +0000
564+++ src/wlapplication.h 2013-07-16 20:53:40 +0000
565@@ -287,21 +287,6 @@
566 ///true if an external entity wants us to quit
567 bool m_should_die;
568
569- ///The Widelands window's width in pixels
570- int32_t m_gfx_w;
571-
572- ///The Widelands window's height in pixels
573- int32_t m_gfx_h;
574-
575- ///The Widelands window's bits per pixel
576- int32_t m_gfx_bpp;
577-
578- ///If true Widelands is (should be, we never know ;-) running
579- ///in a fullscreen window
580- bool m_gfx_fullscreen;
581-
582- bool m_gfx_opengl;
583-
584 //do we want to search the default places for widelands installs
585 bool m_default_datadirs;
586 std::string m_homedir;

Subscribers

People subscribed via source and target branches

to status/vote changes: